From af1f572b8963a3600403d01e14d63fcc14eb7ba6 Mon Sep 17 00:00:00 2001 From: zainab-ali Date: Wed, 22 Apr 2026 14:41:53 +0100 Subject: [PATCH 1/2] Add source location URL to CI test failures. --- docs/features/troubleshooting_failures.md | 10 ++++ .../src/main/scala/weaver/CatsUnsafeRun.scala | 2 + .../src/main/scala/weaver/Formatter.scala | 2 +- .../shared/src/main/scala/weaver/Result.scala | 60 ++++++++++++++----- .../shared/src/main/scala/weaver/Test.scala | 26 ++++---- .../src/main/scala/weaver/TestOutcome.scala | 5 +- .../src/main/scala/weaver/UnsafeRun.scala | 2 + .../weaver/internals/SourceLocationUrl.scala | 29 +++++++++ .../shared/src/main/scala/weaver/suites.scala | 30 ++++++++-- .../scala/weaver/discipline/Discipline.scala | 16 ++++- .../shared/src/test/scala/DogFoodTests.scala | 35 +++++++++++ .../shared/src/test/scala/Meta.scala | 29 +++++++++ .../src/main/scala/RunnerCompat.scala | 2 +- .../jvm/src/main/scala/RunnerCompat.scala | 2 +- 14 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala diff --git a/docs/features/troubleshooting_failures.md b/docs/features/troubleshooting_failures.md index 1e99c4b2..701c7387 100644 --- a/docs/features/troubleshooting_failures.md +++ b/docs/features/troubleshooting_failures.md @@ -193,6 +193,16 @@ object SourceLocationSuite extends FunSuite { println(weaver.docs.Output.runSuites(SourceLocationSuite)) ``` +## Displaying source locations in CI test runs + +Source locations in CI runs are displayed as urls. If you use GitHub Actions, the source code URL is determined automatically. + +You can display urls in other CI providers using the `WEAVER_SOURCE_URL` environment variable. + +```sh +export WEAVER_SOURCE_URL=https://gitlab.com/my-org/my-repo/-/tree/my-branch/ +``` + ## Displaying a trace If you have a test codebase with many nested helper functions, you may want to display a trace of source locations. You can do this with the `traced(here)` function. diff --git a/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala b/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala index cbd44017..2b6d7106 100644 --- a/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala +++ b/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala @@ -4,6 +4,7 @@ import scala.concurrent.Future import cats.effect.unsafe.implicits.global import cats.effect.{ FiberIO, IO } +import cats.effect.std.Env object CatsUnsafeRun extends CatsUnsafeRun @@ -13,6 +14,7 @@ trait CatsUnsafeRun extends UnsafeRun[IO] with CatsUnsafeRunPlatformCompat { override implicit val parallel = IO.parallelForIO override implicit val effect = IO.asyncForIO + override def env: Env[IO] = IO.envForIO def cancel(token: CancelToken): Unit = unsafeRunSync(token.cancel) diff --git a/modules/core/shared/src/main/scala/weaver/Formatter.scala b/modules/core/shared/src/main/scala/weaver/Formatter.scala index c6f69872..9b072c84 100644 --- a/modules/core/shared/src/main/scala/weaver/Formatter.scala +++ b/modules/core/shared/src/main/scala/weaver/Formatter.scala @@ -47,7 +47,7 @@ object Formatter { case Success => withPrefix(green("+ ")) case OnlyTagNotAllowedInCI(_) | Failures(_) | Exception(_) => withPrefix(red("- ")) - case Ignored(_, _) => + case Ignored(_, _, _) => withPrefix(yellow("- ")) + yellow(" !!! IGNORED !!!") } } diff --git a/modules/core/shared/src/main/scala/weaver/Result.scala b/modules/core/shared/src/main/scala/weaver/Result.scala index 49532efe..56b68316 100644 --- a/modules/core/shared/src/main/scala/weaver/Result.scala +++ b/modules/core/shared/src/main/scala/weaver/Result.scala @@ -10,22 +10,29 @@ private[weaver] sealed trait Result { private[weaver] object Result { import Formatter._ - def fromAssertion(assertion: Expectations): Result = assertion.run match { - case Valid(_) => Success - case Invalid(failed) => - Failures(failed.map(ex => - Failures.Failure(ex.message, ex, ex.locations))) - } + def fromAssertion( + sourceLocationUrl: Option[String], + assertion: Expectations): Result = + assertion.run match { + case Valid(_) => Success + case Invalid(failed) => + Failures(failed.map(ex => + Failures.Failure(ex.message, ex, sourceLocationUrl, ex.locations))) + } case object Success extends Result { def formatted: Option[String] = None } - final case class Ignored(reason: String, location: SourceLocation) + final case class Ignored( + reason: String, + sourceLocationUrl: Option[String], + location: SourceLocation) extends Result { def formatted: Option[String] = { Some(formatDescription(reason, + sourceLocationUrl = sourceLocationUrl, List(location), Console.YELLOW, TAB2.prefix)) @@ -40,6 +47,7 @@ private[weaver] object Result { val failure = failures.head val formattedMessage = formatDescription( failure.msg, + failures.head.sourceLocationUrl, failure.locations.toList, Console.RED, TAB2.prefix @@ -53,6 +61,7 @@ private[weaver] object Result { formatDescription( msg, + sourceLocationUrl, locations.toList, Console.RED, s" [$idx] " @@ -67,6 +76,7 @@ private[weaver] object Result { final case class Failure( msg: String, source: ExpectationFailed, + sourceLocationUrl: Option[String], locations: NonEmptyList[SourceLocation]) } @@ -77,6 +87,7 @@ private[weaver] object Result { def formatted: Option[String] = { val formattedMessage = formatDescription( "'Only' tag is not allowed when `isCI=true`", + None, List(location), Console.RED, TAB2.prefix @@ -102,13 +113,13 @@ private[weaver] object Result { val success: Result = Success - def from(error: Throwable): Result = { + def from(sourceLocationUrl: Option[String], error: Throwable): Result = { error match { case ex: IgnoredException => - Ignored(ex.reason, ex.location) + Ignored(ex.reason, sourceLocationUrl, ex.location) case exs: ExpectationsFailed => Failures(exs.failures.map { ex => - Failures.Failure(ex.message, ex, ex.locations) + Failures.Failure(ex.message, ex, sourceLocationUrl, ex.locations) }) case other => Exception(other) @@ -137,6 +148,7 @@ private[weaver] object Result { if (errorOutputLines.nonEmpty) { formatDescription(errorOutputLines.mkString(EOL), + None, Nil, Console.RED, TAB2.prefix) @@ -145,6 +157,7 @@ private[weaver] object Result { val formattedMessage = formatDescription( msg, + None, Nil, Console.RED, TAB2.prefix @@ -159,12 +172,13 @@ private[weaver] object Result { private def formatDescription( message: String, + sourceLocationUrl: Option[String], location: List[SourceLocation], color: String, prefix: String): String = { val prefixIsWhitespace = prefix.trim.isEmpty - val footer = locationFooter(location) + val footer = locationFooter(sourceLocationUrl, location) val lines = (message.split("\\r?\\n") ++ footer).zipWithIndex.map { case (line, index) => val linePrefix = @@ -172,7 +186,7 @@ private[weaver] object Result { if (index == 0) color + linePrefix + line + location - .map(l => s" (${l.fileRelativePath}:${l.line})") + .map(l => s" (${formatLocationPath(sourceLocationUrl, l)})") .mkString("\n") else color + linePrefix + line @@ -181,14 +195,30 @@ private[weaver] object Result { lines.mkString(EOL) + Console.RESET } - private def locationFooter(locations: List[SourceLocation]): List[String] = { + private def locationFooter( + sourceLocationUrl: Option[String], + locations: List[SourceLocation]): List[String] = { val lines = locations.flatMap { l => - val prefix = s"${l.fileRelativePath}:${l.line}" l.sourceCode.fold(List.empty[String]) { sourceCode => val pointer = " " * (sourceCode.column - 1) + "^" - List(prefix, sourceCode.sourceLine, pointer) + List(formatLocationPath(sourceLocationUrl, l), + sourceCode.sourceLine, + pointer) } } if (lines.nonEmpty) "" :: lines else Nil } + + private def formatLocationPath( + sourceLocationUrl: Option[String], + l: SourceLocation): String = + sourceLocationUrl match { + case Some(url) => + // Display a URL to a source location on a CI host. Line numbers are typically referenced with #L anchors. + s"${url}${l.fileRelativePath}#L${l.line}" + case None => + // Display a path to a local file. + s"${l.fileRelativePath}:${l.line}" + } + } diff --git a/modules/core/shared/src/main/scala/weaver/Test.scala b/modules/core/shared/src/main/scala/weaver/Test.scala index c639cc1a..fa0f00f3 100644 --- a/modules/core/shared/src/main/scala/weaver/Test.scala +++ b/modules/core/shared/src/main/scala/weaver/Test.scala @@ -8,21 +8,25 @@ import cats.data.Chain import cats.effect.Ref import cats.effect.Clock import cats.effect.Concurrent +import cats.effect.std.Env import cats.syntax.all._ - +import weaver.internals.SourceLocationUrl object Test { def apply[F[_]](name: String, f: Log[F] => F[Expectations])( implicit F: Defer[F], G: Concurrent[F], - C: Clock[F]): F[TestOutcome] = { + C: Clock[F], + E: Env[F] + ): F[TestOutcome] = { for { - ref <- Ref[F].of(Chain.empty[Log.Entry]) - start <- C.realTime - res <- Defer[F] + ref <- Ref[F].of(Chain.empty[Log.Entry]) + start <- C.realTime + sourceUrl <- SourceLocationUrl[F] + res <- Defer[F] .defer(f(Log.collected[F, Chain](ref, C.realTime.map(_.toMillis)))) - .map(Result.fromAssertion) - .handleError(ex => Result.from(ex)) + .map(Result.fromAssertion(sourceUrl, _)) + .handleError(ex => Result.from(sourceUrl, ex)) end <- C.realTime logs <- ref.get } yield TestOutcome(name, end - start, res, logs) @@ -33,8 +37,8 @@ object Test { val (attempt, duration) = Try(ex()) -> (System.currentTimeMillis() - start) val res = attempt match { - case Success(assertions) => Result.fromAssertion(assertions) - case Failure(e) => Result.from(e) + case Success(assertions) => Result.fromAssertion(None, assertions) + case Failure(e) => Result.from(None, e) } TestOutcome(name, duration.millis, res, Chain.empty) @@ -43,7 +47,7 @@ object Test { def apply[F[_]](name: String, f: F[Expectations])( implicit F: Defer[F], G: Concurrent[F], - C: Clock[F] + C: Clock[F], + E: Env[F] ): F[TestOutcome] = apply[F](name, (_: Log[F]) => f) - } diff --git a/modules/core/shared/src/main/scala/weaver/TestOutcome.scala b/modules/core/shared/src/main/scala/weaver/TestOutcome.scala index c603de14..a925b1b0 100644 --- a/modules/core/shared/src/main/scala/weaver/TestOutcome.scala +++ b/modules/core/shared/src/main/scala/weaver/TestOutcome.scala @@ -35,8 +35,8 @@ object TestOutcome { extends TestOutcome { def status: TestStatus = result match { - case Result.Success => TestStatus.Success - case Result.Ignored(_, _) => TestStatus.Ignored + case Result.Success => TestStatus.Success + case Result.Ignored(_, _, _) => TestStatus.Ignored case Result.OnlyTagNotAllowedInCI(_) | Result.Failures(_) => TestStatus.Failure case Result.Exception(_) => TestStatus.Exception @@ -47,6 +47,7 @@ object TestOutcome { case Result.Failures(failures) => Some(new ExpectationsFailed(failures.map(_.source))) case Result.OnlyTagNotAllowedInCI(_) | Result.Ignored( + _, _, _) | Result.Success => None } diff --git a/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala b/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala index 6fbc629c..15d14e1f 100644 --- a/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala +++ b/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala @@ -5,10 +5,12 @@ import scala.concurrent.duration.FiniteDuration import cats.Parallel import cats.effect.{ Async, Clock } +import cats.effect.std.Env trait EffectCompat[F[_]] { implicit def parallel: Parallel[F] implicit def effect: Async[F] + def env: Env[F] protected[weaver] def clock: Clock[F] = effect private[weaver] final def sleep(duration: FiniteDuration): F[Unit] = diff --git a/modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala b/modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala new file mode 100644 index 00000000..c31b2326 --- /dev/null +++ b/modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala @@ -0,0 +1,29 @@ +package weaver.internals + +import cats.effect.std.Env +import cats.Monad +import cats.syntax.all._ + +/** + * Constructs base URL to use for displaying source locations in failure + * messages. + */ +private[weaver] object SourceLocationUrl { + def apply[F[_]](implicit E: Env[F], F: Monad[F]): F[Option[String]] = { + // Users can support non-GitHub CIs by setting WEAVER_SOURCE_URL + val readWeaverUrl = E.get("WEAVER_SOURCE_URL") + // If tests are run on GitHub Actions, construct a URL to the test source code. + // See https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables + val readGitHubUrl = ( + E.get("GITHUB_SERVER_URL"), + E.get("GITHUB_REPOSITORY"), + E.get("GITHUB_SHA") + ).tupled.map(_.mapN { case (serverUrl, repo, sha) => + s"$serverUrl/$repo/tree/$sha/" + }) + (readWeaverUrl, readGitHubUrl).mapN { + case (weaverUrl, gitHubUrl) => weaverUrl.orElse(gitHubUrl) + } + } + +} diff --git a/modules/core/shared/src/main/scala/weaver/suites.scala b/modules/core/shared/src/main/scala/weaver/suites.scala index 35cf4d8d..5b385468 100644 --- a/modules/core/shared/src/main/scala/weaver/suites.scala +++ b/modules/core/shared/src/main/scala/weaver/suites.scala @@ -15,6 +15,7 @@ trait BaseSuiteClass {} // A version of EffectSuite that has a type member instead of a type parameter. protected[weaver] trait EffectSuiteAux { type EffectType[A] + protected def effectCompat: EffectCompat[EffectType] implicit protected def effect: Async[EffectType] } @@ -158,25 +159,38 @@ private[weaver] object TagAnalysisResult { abstract class MutableFSuite[F[_]] extends SharedResourceSuite[F] { def pureTest(name: TestName)(run: => Expectations): Unit = registerTest(name)(_ => - Test(name.name, effect.delay(run))(effect, effect, effectCompat.clock)) + Test(name.name, effect.delay(run))(effect, + effect, + effectCompat.clock, + effectCompat.env)) def loggedTest(name: TestName)(run: Log[F] => F[Expectations]): Unit = registerTest(name)(_ => - Test[F](name.name, log => run(log))(effect, effect, effectCompat.clock)) + Test[F](name.name, log => run(log))(effect, + effect, + effectCompat.clock, + effectCompat.env)) def test(name: TestName): PartiallyAppliedTest = new PartiallyAppliedTest(name) class PartiallyAppliedTest(name: TestName) { def apply(run: => F[Expectations]): Unit = registerTest(name)(_ => - Test(name.name, run)(effect, effect, effectCompat.clock)) + Test(name.name, run)(effect, + effect, + effectCompat.clock, + effectCompat.env)) def apply(run: Res => F[Expectations]): Unit = registerTest(name)(res => - Test(name.name, run(res))(effect, effect, effectCompat.clock)) + Test(name.name, run(res))(effect, + effect, + effectCompat.clock, + effectCompat.env)) def apply(run: (Res, Log[F]) => F[Expectations]): Unit = registerTest(name)(res => Test[F](name.name, log => run(res, log))(effect, effect, - effectCompat.clock)) + effectCompat.clock, + effectCompat.env)) // this alias helps using pattern matching on `Res` def usingRes(run: Res => F[Expectations]): Unit = apply(run) @@ -189,7 +203,11 @@ abstract class FunSuiteF[F[_]] extends SharedResourceSuite[F] { override final def maxParallelism: Int = 1 def test(name: TestName)(run: => Expectations): Unit = - registerTest(name)(_ => effect.pure(Test.pure(name.name)(() => run))) + registerTest(name)(_ => + Test(name.name, effect.delay(run))(effect, + effect, + effectCompat.clock, + effectCompat.env)) } diff --git a/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala b/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala index 81f98ca9..6852c450 100644 --- a/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala +++ b/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala @@ -27,8 +27,13 @@ trait Discipline { self: SharedResourceSuiteAux => case (id, prop) => val testName = name.copy(s"${name.name}: $id") registerTest(testName) { _ => - effect.pure(Test.pure(testName.name)(() => - executeProp(prop, name.location, parameters))) + Test(testName.name, + effect.delay(executeProp(prop, name.location, parameters)))( + effect, + effect, + effectCompat.clock, + effectCompat.env + ) } } @@ -78,7 +83,12 @@ trait DisciplineFSuite[F[_]] extends DisciplineFRunnableSuite[F] { foundProps.synchronized { foundProps += name.copy(propTestName) } - Test(propTestName, runProp) + Test(propTestName, runProp)( + effect, + effect, + effectCompat.clock, + effectCompat.env + ) }).run ) } diff --git a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala index 709f8920..26005ec1 100644 --- a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala +++ b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala @@ -329,6 +329,41 @@ object DogFoodTests extends IOSuite { } } + test("source locations are rendered with URLs") { + _.runSuite(Meta.SourceUrlSuite).flatMap { + case (logs, _) => + val actual = extractFailureMessageForTest(logs, "(failure)") + if (ScalaCompat.isScala3) + assertInlineSnapshot( + actual, + """- (failure) 0ms + Values not equal: (https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52) + + in expect.eql(- expected, + found) + -1 + +2 + + https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52 + IO(expect.eql(1, 2)) + ^""" + ) + else + assertInlineSnapshot( + actual, + """- (failure) 0ms + Values not equal: (https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52) + + in expect.eql(- expected, + found) + -1 + +2 + + https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52 + IO(expect.eql(1, 2)) + ^""" + ) + } + } + test("successes with clues are rendered correctly") { _.runSuite(Meta.Clue).flatMap { case (logs, _) => diff --git a/modules/framework-cats/shared/src/test/scala/Meta.scala b/modules/framework-cats/shared/src/test/scala/Meta.scala index bf7c920b..1687d8fd 100644 --- a/modules/framework-cats/shared/src/test/scala/Meta.scala +++ b/modules/framework-cats/shared/src/test/scala/Meta.scala @@ -45,6 +45,14 @@ object Meta { } } + object SourceUrlSuite extends SimpleIOSuite { + override protected def effectCompat: UnsafeRun[IO] = SourceUrlUnsafeRun + + test("(failure)") { + IO(expect.eql(1, 2)) + } + } + object MutableSuiteTest extends MutableSuiteTest object Boom extends Error("Boom") with scala.util.control.NoStackTrace @@ -330,6 +338,7 @@ object Meta { object SetTimeUnsafeRun extends CatsUnsafeRun { import scala.concurrent.duration._ + import cats.effect.std.Env private val setTimestamp = weaver.internals.Timestamp.localTime(12, 54, 35) @@ -338,6 +347,26 @@ object Meta { def monotonic: cats.effect.IO[FiniteDuration] = IO(0L.millis) def applicative: cats.Applicative[cats.effect.IO] = cats.Applicative[IO] } + + // Override the environment variables such that local error messages are displayed in CI runs. + override def env: Env[IO] = new Env[IO] { + def entries: IO[List[(String, String)]] = IO.pure(Nil) + def get(name: String): IO[Option[String]] = IO.pure(None) + } + } + + object SourceUrlUnsafeRun extends CatsUnsafeRun { + import cats.effect.std.Env + + override def clock: Clock[IO] = SetTimeUnsafeRun.clock + + override def env: Env[IO] = new Env[IO] { + def entries: IO[List[(String, String)]] = IO(List( + ("WEAVER_SOURCE_URL", + "https://github.com/typelevel/weaver-test/blob/v0.12.0/") + )) + def get(name: String): IO[Option[String]] = entries.map(_.toMap.get(name)) + } } } diff --git a/modules/framework/js-native/src/main/scala/RunnerCompat.scala b/modules/framework/js-native/src/main/scala/RunnerCompat.scala index a1c79d64..99100aed 100644 --- a/modules/framework/js-native/src/main/scala/RunnerCompat.scala +++ b/modules/framework/js-native/src/main/scala/RunnerCompat.scala @@ -131,7 +131,7 @@ trait RunnerCompat[F[_]] { self: sbt.testing.Runner => val outcome = TestOutcome("Unexpected failure", 0.seconds, - Result.from(error), + Result.from(sourceLocationUrl = None, error), Chain.empty) reportTest(outcome).productR( reportDoneF(TestOutcomeNative.from(fqn)(outcome))) diff --git a/modules/framework/jvm/src/main/scala/RunnerCompat.scala b/modules/framework/jvm/src/main/scala/RunnerCompat.scala index cce720e6..2e9ceca5 100644 --- a/modules/framework/jvm/src/main/scala/RunnerCompat.scala +++ b/modules/framework/jvm/src/main/scala/RunnerCompat.scala @@ -233,7 +233,7 @@ trait RunnerCompat[F[_]] { self: sbt.testing.Runner => val outcome = TestOutcome("Unexpected failure", 0.seconds, - Result.from(error), + Result.from(sourceLocationUrl = None, error), Chain.empty) Async[F].guarantee(outcomes From 727198111b4f42e1369fdafbafc7e14e78ba4dc1 Mon Sep 17 00:00:00 2001 From: zainab-ali Date: Mon, 27 Apr 2026 11:20:42 +0100 Subject: [PATCH 2/2] Use unsafe function to get source location instead of Env typeclass. --- build.sbt | 9 +- .../src/main/scala/weaver/CatsUnsafeRun.scala | 2 - .../src/main/scala/weaver/internals/Env.scala | 16 +++ .../src/main/scala/weaver/internals/Env.scala | 5 + .../src/main/scala/weaver/Formatter.scala | 2 +- .../shared/src/main/scala/weaver/Result.scala | 51 +++---- .../shared/src/main/scala/weaver/Test.scala | 26 ++-- .../src/main/scala/weaver/TestOutcome.scala | 5 +- .../src/main/scala/weaver/UnsafeRun.scala | 2 - .../weaver/internals/SourceLocationUrl.scala | 23 ++- .../shared/src/main/scala/weaver/suites.scala | 30 +--- .../scala/weaver/discipline/Discipline.scala | 16 +-- .../src/test/scala/Snapshot4sStubs.scala | 2 +- .../test/scala/junit/JUnitRunnerTests.scala | 8 +- .../shared/src/test/scala/DogFoodTests.scala | 131 +++++++----------- .../shared/src/test/scala/Meta.scala | 29 ---- .../src/test/scala/TagDogFoodTests.scala | 4 +- .../src/main/scala/RunnerCompat.scala | 2 +- .../jvm/src/main/scala/RunnerCompat.scala | 2 +- 19 files changed, 135 insertions(+), 230 deletions(-) create mode 100644 modules/core/js/src/main/scala/weaver/internals/Env.scala create mode 100644 modules/core/jvm-native/src/main/scala/weaver/internals/Env.scala diff --git a/build.sbt b/build.sbt index 219742b6..ff8d7715 100644 --- a/build.sbt +++ b/build.sbt @@ -168,12 +168,17 @@ lazy val cats = crossProject(JVMPlatform, JSPlatform, NativePlatform) .dependsOn(framework, coreCats) .settings( name := "weaver-cats", - testFrameworks := Seq(new TestFramework("weaver.framework.CatsEffect")) + testFrameworks := Seq(new TestFramework("weaver.framework.CatsEffect")), + // Ensure that the source locations in failure messages are identical on CI + // as when running locally. See `weaver.internals.SourceLocationUrl`. + Test / envVars := Map("WEAVER_SOURCE_URL" -> "") ) lazy val catsJVM = cats.jvm .settings( libraryDependencies += - "com.siriusxm" %% "snapshot4s-core" % Version.snapshot4s % Test + "com.siriusxm" %% "snapshot4s-core" % Version.snapshot4s % Test, + // Required for seting the WEAVER_SOURCE_URL environment variable. + Test / fork := true ) .enablePlugins(Snapshot4sPlugin) diff --git a/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala b/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala index 2b6d7106..cbd44017 100644 --- a/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala +++ b/modules/core-cats/shared/src/main/scala/weaver/CatsUnsafeRun.scala @@ -4,7 +4,6 @@ import scala.concurrent.Future import cats.effect.unsafe.implicits.global import cats.effect.{ FiberIO, IO } -import cats.effect.std.Env object CatsUnsafeRun extends CatsUnsafeRun @@ -14,7 +13,6 @@ trait CatsUnsafeRun extends UnsafeRun[IO] with CatsUnsafeRunPlatformCompat { override implicit val parallel = IO.parallelForIO override implicit val effect = IO.asyncForIO - override def env: Env[IO] = IO.envForIO def cancel(token: CancelToken): Unit = unsafeRunSync(token.cancel) diff --git a/modules/core/js/src/main/scala/weaver/internals/Env.scala b/modules/core/js/src/main/scala/weaver/internals/Env.scala new file mode 100644 index 00000000..5a05de7c --- /dev/null +++ b/modules/core/js/src/main/scala/weaver/internals/Env.scala @@ -0,0 +1,16 @@ +package weaver.internals + +import scala.scalajs.js +import scala.util.Try + +private[weaver] object Env { + def get(name: String): Option[String] = processEnv.get(name).collect { + case value: String => value + } + + // Attempt to read NodeJS environment variables + private def processEnv: js.Dictionary[Any] = + Try(js.Dynamic.global.process.env.asInstanceOf[js.Dictionary[Any]]) + .getOrElse(js.Dictionary.empty) + +} diff --git a/modules/core/jvm-native/src/main/scala/weaver/internals/Env.scala b/modules/core/jvm-native/src/main/scala/weaver/internals/Env.scala new file mode 100644 index 00000000..e1236f16 --- /dev/null +++ b/modules/core/jvm-native/src/main/scala/weaver/internals/Env.scala @@ -0,0 +1,5 @@ +package weaver.internals + +private[weaver] object Env { + def get(key: String): Option[String] = sys.env.get(key) +} diff --git a/modules/core/shared/src/main/scala/weaver/Formatter.scala b/modules/core/shared/src/main/scala/weaver/Formatter.scala index 9b072c84..c6f69872 100644 --- a/modules/core/shared/src/main/scala/weaver/Formatter.scala +++ b/modules/core/shared/src/main/scala/weaver/Formatter.scala @@ -47,7 +47,7 @@ object Formatter { case Success => withPrefix(green("+ ")) case OnlyTagNotAllowedInCI(_) | Failures(_) | Exception(_) => withPrefix(red("- ")) - case Ignored(_, _, _) => + case Ignored(_, _) => withPrefix(yellow("- ")) + yellow(" !!! IGNORED !!!") } } diff --git a/modules/core/shared/src/main/scala/weaver/Result.scala b/modules/core/shared/src/main/scala/weaver/Result.scala index 56b68316..8698e110 100644 --- a/modules/core/shared/src/main/scala/weaver/Result.scala +++ b/modules/core/shared/src/main/scala/weaver/Result.scala @@ -2,6 +2,7 @@ package weaver import cats.data.NonEmptyList import cats.data.Validated.{ Invalid, Valid } +import weaver.internals.SourceLocationUrl private[weaver] sealed trait Result { def formatted: Option[String] @@ -10,29 +11,22 @@ private[weaver] sealed trait Result { private[weaver] object Result { import Formatter._ - def fromAssertion( - sourceLocationUrl: Option[String], - assertion: Expectations): Result = - assertion.run match { - case Valid(_) => Success - case Invalid(failed) => - Failures(failed.map(ex => - Failures.Failure(ex.message, ex, sourceLocationUrl, ex.locations))) - } + def fromAssertion(assertion: Expectations): Result = assertion.run match { + case Valid(_) => Success + case Invalid(failed) => + Failures(failed.map(ex => + Failures.Failure(ex.message, ex, ex.locations))) + } case object Success extends Result { def formatted: Option[String] = None } - final case class Ignored( - reason: String, - sourceLocationUrl: Option[String], - location: SourceLocation) + final case class Ignored(reason: String, location: SourceLocation) extends Result { def formatted: Option[String] = { Some(formatDescription(reason, - sourceLocationUrl = sourceLocationUrl, List(location), Console.YELLOW, TAB2.prefix)) @@ -47,7 +41,6 @@ private[weaver] object Result { val failure = failures.head val formattedMessage = formatDescription( failure.msg, - failures.head.sourceLocationUrl, failure.locations.toList, Console.RED, TAB2.prefix @@ -61,7 +54,6 @@ private[weaver] object Result { formatDescription( msg, - sourceLocationUrl, locations.toList, Console.RED, s" [$idx] " @@ -76,7 +68,6 @@ private[weaver] object Result { final case class Failure( msg: String, source: ExpectationFailed, - sourceLocationUrl: Option[String], locations: NonEmptyList[SourceLocation]) } @@ -87,7 +78,6 @@ private[weaver] object Result { def formatted: Option[String] = { val formattedMessage = formatDescription( "'Only' tag is not allowed when `isCI=true`", - None, List(location), Console.RED, TAB2.prefix @@ -113,13 +103,13 @@ private[weaver] object Result { val success: Result = Success - def from(sourceLocationUrl: Option[String], error: Throwable): Result = { + def from(error: Throwable): Result = { error match { case ex: IgnoredException => - Ignored(ex.reason, sourceLocationUrl, ex.location) + Ignored(ex.reason, ex.location) case exs: ExpectationsFailed => Failures(exs.failures.map { ex => - Failures.Failure(ex.message, ex, sourceLocationUrl, ex.locations) + Failures.Failure(ex.message, ex, ex.locations) }) case other => Exception(other) @@ -148,7 +138,6 @@ private[weaver] object Result { if (errorOutputLines.nonEmpty) { formatDescription(errorOutputLines.mkString(EOL), - None, Nil, Console.RED, TAB2.prefix) @@ -157,7 +146,6 @@ private[weaver] object Result { val formattedMessage = formatDescription( msg, - None, Nil, Console.RED, TAB2.prefix @@ -172,13 +160,12 @@ private[weaver] object Result { private def formatDescription( message: String, - sourceLocationUrl: Option[String], location: List[SourceLocation], color: String, prefix: String): String = { val prefixIsWhitespace = prefix.trim.isEmpty - val footer = locationFooter(sourceLocationUrl, location) + val footer = locationFooter(location) val lines = (message.split("\\r?\\n") ++ footer).zipWithIndex.map { case (line, index) => val linePrefix = @@ -186,7 +173,7 @@ private[weaver] object Result { if (index == 0) color + linePrefix + line + location - .map(l => s" (${formatLocationPath(sourceLocationUrl, l)})") + .map(l => s" (${formatLocationPath(l)})") .mkString("\n") else color + linePrefix + line @@ -195,13 +182,11 @@ private[weaver] object Result { lines.mkString(EOL) + Console.RESET } - private def locationFooter( - sourceLocationUrl: Option[String], - locations: List[SourceLocation]): List[String] = { + private def locationFooter(locations: List[SourceLocation]): List[String] = { val lines = locations.flatMap { l => l.sourceCode.fold(List.empty[String]) { sourceCode => val pointer = " " * (sourceCode.column - 1) + "^" - List(formatLocationPath(sourceLocationUrl, l), + List(formatLocationPath(l), sourceCode.sourceLine, pointer) } @@ -209,10 +194,8 @@ private[weaver] object Result { if (lines.nonEmpty) "" :: lines else Nil } - private def formatLocationPath( - sourceLocationUrl: Option[String], - l: SourceLocation): String = - sourceLocationUrl match { + private def formatLocationPath(l: SourceLocation): String = + SourceLocationUrl() match { case Some(url) => // Display a URL to a source location on a CI host. Line numbers are typically referenced with #L anchors. s"${url}${l.fileRelativePath}#L${l.line}" diff --git a/modules/core/shared/src/main/scala/weaver/Test.scala b/modules/core/shared/src/main/scala/weaver/Test.scala index fa0f00f3..c639cc1a 100644 --- a/modules/core/shared/src/main/scala/weaver/Test.scala +++ b/modules/core/shared/src/main/scala/weaver/Test.scala @@ -8,25 +8,21 @@ import cats.data.Chain import cats.effect.Ref import cats.effect.Clock import cats.effect.Concurrent -import cats.effect.std.Env import cats.syntax.all._ -import weaver.internals.SourceLocationUrl + object Test { def apply[F[_]](name: String, f: Log[F] => F[Expectations])( implicit F: Defer[F], G: Concurrent[F], - C: Clock[F], - E: Env[F] - ): F[TestOutcome] = { + C: Clock[F]): F[TestOutcome] = { for { - ref <- Ref[F].of(Chain.empty[Log.Entry]) - start <- C.realTime - sourceUrl <- SourceLocationUrl[F] - res <- Defer[F] + ref <- Ref[F].of(Chain.empty[Log.Entry]) + start <- C.realTime + res <- Defer[F] .defer(f(Log.collected[F, Chain](ref, C.realTime.map(_.toMillis)))) - .map(Result.fromAssertion(sourceUrl, _)) - .handleError(ex => Result.from(sourceUrl, ex)) + .map(Result.fromAssertion) + .handleError(ex => Result.from(ex)) end <- C.realTime logs <- ref.get } yield TestOutcome(name, end - start, res, logs) @@ -37,8 +33,8 @@ object Test { val (attempt, duration) = Try(ex()) -> (System.currentTimeMillis() - start) val res = attempt match { - case Success(assertions) => Result.fromAssertion(None, assertions) - case Failure(e) => Result.from(None, e) + case Success(assertions) => Result.fromAssertion(assertions) + case Failure(e) => Result.from(e) } TestOutcome(name, duration.millis, res, Chain.empty) @@ -47,7 +43,7 @@ object Test { def apply[F[_]](name: String, f: F[Expectations])( implicit F: Defer[F], G: Concurrent[F], - C: Clock[F], - E: Env[F] + C: Clock[F] ): F[TestOutcome] = apply[F](name, (_: Log[F]) => f) + } diff --git a/modules/core/shared/src/main/scala/weaver/TestOutcome.scala b/modules/core/shared/src/main/scala/weaver/TestOutcome.scala index a925b1b0..c603de14 100644 --- a/modules/core/shared/src/main/scala/weaver/TestOutcome.scala +++ b/modules/core/shared/src/main/scala/weaver/TestOutcome.scala @@ -35,8 +35,8 @@ object TestOutcome { extends TestOutcome { def status: TestStatus = result match { - case Result.Success => TestStatus.Success - case Result.Ignored(_, _, _) => TestStatus.Ignored + case Result.Success => TestStatus.Success + case Result.Ignored(_, _) => TestStatus.Ignored case Result.OnlyTagNotAllowedInCI(_) | Result.Failures(_) => TestStatus.Failure case Result.Exception(_) => TestStatus.Exception @@ -47,7 +47,6 @@ object TestOutcome { case Result.Failures(failures) => Some(new ExpectationsFailed(failures.map(_.source))) case Result.OnlyTagNotAllowedInCI(_) | Result.Ignored( - _, _, _) | Result.Success => None } diff --git a/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala b/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala index 15d14e1f..6fbc629c 100644 --- a/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala +++ b/modules/core/shared/src/main/scala/weaver/UnsafeRun.scala @@ -5,12 +5,10 @@ import scala.concurrent.duration.FiniteDuration import cats.Parallel import cats.effect.{ Async, Clock } -import cats.effect.std.Env trait EffectCompat[F[_]] { implicit def parallel: Parallel[F] implicit def effect: Async[F] - def env: Env[F] protected[weaver] def clock: Clock[F] = effect private[weaver] final def sleep(duration: FiniteDuration): F[Unit] = diff --git a/modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala b/modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala index c31b2326..64d70cf0 100644 --- a/modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala +++ b/modules/core/shared/src/main/scala/weaver/internals/SourceLocationUrl.scala @@ -1,7 +1,5 @@ package weaver.internals -import cats.effect.std.Env -import cats.Monad import cats.syntax.all._ /** @@ -9,21 +7,20 @@ import cats.syntax.all._ * messages. */ private[weaver] object SourceLocationUrl { - def apply[F[_]](implicit E: Env[F], F: Monad[F]): F[Option[String]] = { + + def apply(): Option[String] = { // Users can support non-GitHub CIs by setting WEAVER_SOURCE_URL - val readWeaverUrl = E.get("WEAVER_SOURCE_URL") + def readWeaverUrl(): Option[String] = Env.get("WEAVER_SOURCE_URL") // If tests are run on GitHub Actions, construct a URL to the test source code. // See https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables - val readGitHubUrl = ( - E.get("GITHUB_SERVER_URL"), - E.get("GITHUB_REPOSITORY"), - E.get("GITHUB_SHA") - ).tupled.map(_.mapN { case (serverUrl, repo, sha) => + def readGitHubUrl(): Option[String] = ( + Env.get("GITHUB_SERVER_URL"), + Env.get("GITHUB_REPOSITORY"), + Env.get("GITHUB_SHA") + ).mapN { case (serverUrl, repo, sha) => s"$serverUrl/$repo/tree/$sha/" - }) - (readWeaverUrl, readGitHubUrl).mapN { - case (weaverUrl, gitHubUrl) => weaverUrl.orElse(gitHubUrl) } - } + readWeaverUrl().orElse(readGitHubUrl()) + } } diff --git a/modules/core/shared/src/main/scala/weaver/suites.scala b/modules/core/shared/src/main/scala/weaver/suites.scala index 5b385468..35cf4d8d 100644 --- a/modules/core/shared/src/main/scala/weaver/suites.scala +++ b/modules/core/shared/src/main/scala/weaver/suites.scala @@ -15,7 +15,6 @@ trait BaseSuiteClass {} // A version of EffectSuite that has a type member instead of a type parameter. protected[weaver] trait EffectSuiteAux { type EffectType[A] - protected def effectCompat: EffectCompat[EffectType] implicit protected def effect: Async[EffectType] } @@ -159,38 +158,25 @@ private[weaver] object TagAnalysisResult { abstract class MutableFSuite[F[_]] extends SharedResourceSuite[F] { def pureTest(name: TestName)(run: => Expectations): Unit = registerTest(name)(_ => - Test(name.name, effect.delay(run))(effect, - effect, - effectCompat.clock, - effectCompat.env)) + Test(name.name, effect.delay(run))(effect, effect, effectCompat.clock)) def loggedTest(name: TestName)(run: Log[F] => F[Expectations]): Unit = registerTest(name)(_ => - Test[F](name.name, log => run(log))(effect, - effect, - effectCompat.clock, - effectCompat.env)) + Test[F](name.name, log => run(log))(effect, effect, effectCompat.clock)) def test(name: TestName): PartiallyAppliedTest = new PartiallyAppliedTest(name) class PartiallyAppliedTest(name: TestName) { def apply(run: => F[Expectations]): Unit = registerTest(name)(_ => - Test(name.name, run)(effect, - effect, - effectCompat.clock, - effectCompat.env)) + Test(name.name, run)(effect, effect, effectCompat.clock)) def apply(run: Res => F[Expectations]): Unit = registerTest(name)(res => - Test(name.name, run(res))(effect, - effect, - effectCompat.clock, - effectCompat.env)) + Test(name.name, run(res))(effect, effect, effectCompat.clock)) def apply(run: (Res, Log[F]) => F[Expectations]): Unit = registerTest(name)(res => Test[F](name.name, log => run(res, log))(effect, effect, - effectCompat.clock, - effectCompat.env)) + effectCompat.clock)) // this alias helps using pattern matching on `Res` def usingRes(run: Res => F[Expectations]): Unit = apply(run) @@ -203,11 +189,7 @@ abstract class FunSuiteF[F[_]] extends SharedResourceSuite[F] { override final def maxParallelism: Int = 1 def test(name: TestName)(run: => Expectations): Unit = - registerTest(name)(_ => - Test(name.name, effect.delay(run))(effect, - effect, - effectCompat.clock, - effectCompat.env)) + registerTest(name)(_ => effect.pure(Test.pure(name.name)(() => run))) } diff --git a/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala b/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala index 6852c450..81f98ca9 100644 --- a/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala +++ b/modules/discipline/shared/src/main/scala/weaver/discipline/Discipline.scala @@ -27,13 +27,8 @@ trait Discipline { self: SharedResourceSuiteAux => case (id, prop) => val testName = name.copy(s"${name.name}: $id") registerTest(testName) { _ => - Test(testName.name, - effect.delay(executeProp(prop, name.location, parameters)))( - effect, - effect, - effectCompat.clock, - effectCompat.env - ) + effect.pure(Test.pure(testName.name)(() => + executeProp(prop, name.location, parameters))) } } @@ -83,12 +78,7 @@ trait DisciplineFSuite[F[_]] extends DisciplineFRunnableSuite[F] { foundProps.synchronized { foundProps += name.copy(propTestName) } - Test(propTestName, runProp)( - effect, - effect, - effectCompat.clock, - effectCompat.env - ) + Test(propTestName, runProp) }).run ) } diff --git a/modules/framework-cats/js-native/src/test/scala/Snapshot4sStubs.scala b/modules/framework-cats/js-native/src/test/scala/Snapshot4sStubs.scala index 0429005e..3e53098d 100644 --- a/modules/framework-cats/js-native/src/test/scala/Snapshot4sStubs.scala +++ b/modules/framework-cats/js-native/src/test/scala/Snapshot4sStubs.scala @@ -24,5 +24,5 @@ object SnapshotExpectations { implicit @unused config: snapshot4s.SnapshotConfig, comparison: Comparison[A] ): IO[Expectations] = - IO(Expectations.Helpers.expect.eql(found, snapshot)) + IO(Expectations.Helpers.expect.eql(snapshot, found)) } diff --git a/modules/framework-cats/jvm/src/test/scala/junit/JUnitRunnerTests.scala b/modules/framework-cats/jvm/src/test/scala/junit/JUnitRunnerTests.scala index 640ff0ad..20cb03d0 100644 --- a/modules/framework-cats/jvm/src/test/scala/junit/JUnitRunnerTests.scala +++ b/modules/framework-cats/jvm/src/test/scala/junit/JUnitRunnerTests.scala @@ -60,9 +60,9 @@ object JUnitRunnerTests extends SimpleIOSuite { "modules/framework-cats/jvm/src/test/scala/junit/Meta.scala" val message = s"""- $name 0ms - | 'Only' tag is not allowed when `isCI=true` ($srcPath:$lineNumber) + | 'Only' tag is not allowed when `isCI=true` ($srcPath#L$lineNumber) | - | $srcPath:$lineNumber + | $srcPath#L$lineNumber |${sourceCode.trim.stripMargin} | |""".stripMargin @@ -169,9 +169,9 @@ object JUnitRunnerTests extends SimpleIOSuite { """ val message = s"""- $name 0ms - | 'Only' tag is not allowed when `isCI=true` ($srcPath:$lineNumber) + | 'Only' tag is not allowed when `isCI=true` ($srcPath#L$lineNumber) | - | $srcPath:$lineNumber + | $srcPath#L$lineNumber |${sourceCode.trim.stripMargin} | |""".stripMargin diff --git a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala index 26005ec1..9876cf8c 100644 --- a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala +++ b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala @@ -68,7 +68,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (failure) 0ms - | expected (src/main/DogFoodTests.scala:5) + | expected (src/main/DogFoodTests.scala#L5) | | [INFO] 12:54:35 [DogFoodTests.scala:5] this test | [ERROR] 12:54:35 [DogFoodTests.scala:5] has failed @@ -87,9 +87,9 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (multiple-failures) 0ms - | [0] expected (src/main/DogFoodTests.scala:5) + | [0] expected (src/main/DogFoodTests.scala#L5) | - | [1] another (src/main/DogFoodTests.scala:5) + | [1] another (src/main/DogFoodTests.scala#L5) | | [INFO] 12:54:35 [DogFoodTests.scala:5] this test | [ERROR] 12:54:35 [DogFoodTests.scala:5] has failed @@ -163,7 +163,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (failure) 0ms - expected (src/main/DogFoodTests.scala:5) + expected (src/main/DogFoodTests.scala#L5) [ERROR] 12:54:35 [DogFoodTests.scala:5] error weaver.framework.test.Meta$CustomException: surfaced error @@ -188,7 +188,7 @@ object DogFoodTests extends IOSuite { | of | multiline | (failure) - | assertion failed (src/main/DogFoodTests.scala:5) + | assertion failed (src/main/DogFoodTests.scala#L5) | | expect(clue(x) == y) | @@ -230,7 +230,7 @@ object DogFoodTests extends IOSuite { | of | multiline | (ignored) !!! IGNORED !!! - | Ignore me (src/main/DogFoodTests.scala:5)""".stripMargin + | Ignore me (src/main/DogFoodTests.scala#L5)""".stripMargin ) } } @@ -243,7 +243,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (eql Comparison) 0ms - | Values not equal: (src/main/DogFoodTests.scala:5) + | Values not equal: (src/main/DogFoodTests.scala#L5) | | in expect.eql(- expected, + found) | s: foo @@ -262,7 +262,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (same Comparison) 0ms - | Values not equal: (src/main/DogFoodTests.scala:5) + | Values not equal: (src/main/DogFoodTests.scala#L5) | | in expect.same(- expected, + found) | s: foo @@ -280,7 +280,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (eql Show) 0ms - Values not equal: (src/main/DogFoodTests.scala:5) + Values not equal: (src/main/DogFoodTests.scala#L5) in expect.eql(expected, found) Values have the same string representation. Consider modifying their Show instance. @@ -297,7 +297,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (interpolator) 0ms - assertion failed (src/main/DogFoodTests.scala:5) + assertion failed (src/main/DogFoodTests.scala#L5) expect(s"$x" == "2") @@ -314,13 +314,13 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (failFast) 0ms - | [0] Values not equal: (src/main/DogFoodTests.scala:5) + | [0] Values not equal: (src/main/DogFoodTests.scala#L5) | [0] | [0] in expect.eql(- expected, + found) | [0] -1 | [0] +2 | - | [1] Values not equal: (src/main/DogFoodTests.scala:5) + | [1] Values not equal: (src/main/DogFoodTests.scala#L5) | [1] | [1] in expect.eql(- expected, + found) | [1] -3 @@ -329,41 +329,6 @@ object DogFoodTests extends IOSuite { } } - test("source locations are rendered with URLs") { - _.runSuite(Meta.SourceUrlSuite).flatMap { - case (logs, _) => - val actual = extractFailureMessageForTest(logs, "(failure)") - if (ScalaCompat.isScala3) - assertInlineSnapshot( - actual, - """- (failure) 0ms - Values not equal: (https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52) - - in expect.eql(- expected, + found) - -1 - +2 - - https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52 - IO(expect.eql(1, 2)) - ^""" - ) - else - assertInlineSnapshot( - actual, - """- (failure) 0ms - Values not equal: (https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52) - - in expect.eql(- expected, + found) - -1 - +2 - - https://github.com/typelevel/weaver-test/blob/v0.12.0/modules/framework-cats/shared/src/test/scala/Meta.scala#L52 - IO(expect.eql(1, 2)) - ^""" - ) - } - } - test("successes with clues are rendered correctly") { _.runSuite(Meta.Clue).flatMap { case (logs, _) => @@ -382,7 +347,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (failure) 0ms - | assertion failed (src/main/DogFoodTests.scala:5) + | assertion failed (src/main/DogFoodTests.scala#L5) | | expect(clue(x) == clue(y)) | @@ -401,7 +366,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (nested) 0ms - | assertion failed (src/main/DogFoodTests.scala:5) + | assertion failed (src/main/DogFoodTests.scala#L5) | | expect(clue(List(clue(x), clue(y))) == List(x, x)) | @@ -422,7 +387,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (map) 0ms - | assertion failed (src/main/DogFoodTests.scala:5) + | assertion failed (src/main/DogFoodTests.scala#L5) | | expect(List(x, y).map(v => clue(v)) == List(x, x)) | @@ -440,7 +405,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (all) 0ms - | [0] assertion failed (src/main/DogFoodTests.scala:5) + | [0] assertion failed (src/main/DogFoodTests.scala#L5) | [0] | [0] clue(x) == clue(y) | [0] @@ -449,7 +414,7 @@ object DogFoodTests extends IOSuite { | [0] y: Int = 2 | [0] } | - | [1] assertion failed (src/main/DogFoodTests.scala:5) + | [1] assertion failed (src/main/DogFoodTests.scala#L5) | [1] | [1] clue(y) == clue(z) | [1] @@ -468,7 +433,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (show) 0ms - | assertion failed (src/main/DogFoodTests.scala:5) + | assertion failed (src/main/DogFoodTests.scala#L5) | | expect(clue(x) == clue(y)) | @@ -487,7 +452,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (show-from-to-string) 0ms - | assertion failed (src/main/DogFoodTests.scala:5) + | assertion failed (src/main/DogFoodTests.scala#L5) | | expect(clue(x) == clue(y)) | @@ -505,7 +470,7 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, s"""- (helpers) 0ms - | assertion failed (src/main/DogFoodTests.scala:5) + | assertion failed (src/main/DogFoodTests.scala#L5) | | expect(CustomHelpers.clue(x) == otherclue(y) || x == clue(z)) | @@ -526,13 +491,13 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (expect-same) 0ms - Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:22) + Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L22) in expect.same(- expected, + found) -1 +2 - modules/framework-cats/shared/src/test/scala/Meta.scala:22 + modules/framework-cats/shared/src/test/scala/Meta.scala#L22 expect.same(x, y) ^""" ) @@ -540,13 +505,13 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (expect-same) 0ms - Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:22) + Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L22) in expect.same(- expected, + found) -1 +2 - modules/framework-cats/shared/src/test/scala/Meta.scala:22 + modules/framework-cats/shared/src/test/scala/Meta.scala#L22 expect.same(x, y) ^""" ) @@ -561,23 +526,23 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (multiple) 0ms - [0] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:29) + [0] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L29) [0] [0] in expect.same(- expected, + found) [0] -1 [0] +2 [0] - [0] modules/framework-cats/shared/src/test/scala/Meta.scala:29 + [0] modules/framework-cats/shared/src/test/scala/Meta.scala#L29 [0] expect.same(x, y) && expect.same(y, z) [0] ^ - [1] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:29) + [1] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L29) [1] [1] in expect.same(- expected, + found) [1] -2 [1] +3 [1] - [1] modules/framework-cats/shared/src/test/scala/Meta.scala:29 + [1] modules/framework-cats/shared/src/test/scala/Meta.scala#L29 [1] expect.same(x, y) && expect.same(y, z) [1] ^""" ) @@ -585,23 +550,23 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (multiple) 0ms - [0] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:29) + [0] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L29) [0] [0] in expect.same(- expected, + found) [0] -1 [0] +2 [0] - [0] modules/framework-cats/shared/src/test/scala/Meta.scala:29 + [0] modules/framework-cats/shared/src/test/scala/Meta.scala#L29 [0] expect.same(x, y) && expect.same(y, z) [0] ^ - [1] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:29) + [1] Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L29) [1] [1] in expect.same(- expected, + found) [1] -2 [1] +3 [1] - [1] modules/framework-cats/shared/src/test/scala/Meta.scala:29 + [1] modules/framework-cats/shared/src/test/scala/Meta.scala#L29 [1] expect.same(x, y) && expect.same(y, z) [1] ^""" ) @@ -616,21 +581,21 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (traced) 0ms - Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:33) - (modules/framework-cats/shared/src/test/scala/Meta.scala:40) - (modules/framework-cats/shared/src/test/scala/Meta.scala:37) + Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L33) + (modules/framework-cats/shared/src/test/scala/Meta.scala#L40) + (modules/framework-cats/shared/src/test/scala/Meta.scala#L37) in expect.same(- expected, + found) -1 +2 - modules/framework-cats/shared/src/test/scala/Meta.scala:33 + modules/framework-cats/shared/src/test/scala/Meta.scala#L33 helper ^ - modules/framework-cats/shared/src/test/scala/Meta.scala:40 + modules/framework-cats/shared/src/test/scala/Meta.scala#L40 expect.same(1, 2).traced(here) ^ - modules/framework-cats/shared/src/test/scala/Meta.scala:37 + modules/framework-cats/shared/src/test/scala/Meta.scala#L37 nestedHelper.traced(here) ^""" ) @@ -638,21 +603,21 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (traced) 0ms - Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala:33) - (modules/framework-cats/shared/src/test/scala/Meta.scala:40) - (modules/framework-cats/shared/src/test/scala/Meta.scala:37) + Values not equal: (modules/framework-cats/shared/src/test/scala/Meta.scala#L33) + (modules/framework-cats/shared/src/test/scala/Meta.scala#L40) + (modules/framework-cats/shared/src/test/scala/Meta.scala#L37) in expect.same(- expected, + found) -1 +2 - modules/framework-cats/shared/src/test/scala/Meta.scala:33 + modules/framework-cats/shared/src/test/scala/Meta.scala#L33 helper ^ - modules/framework-cats/shared/src/test/scala/Meta.scala:40 + modules/framework-cats/shared/src/test/scala/Meta.scala#L40 expect.same(1, 2).traced(here) ^ - modules/framework-cats/shared/src/test/scala/Meta.scala:37 + modules/framework-cats/shared/src/test/scala/Meta.scala#L37 nestedHelper.traced(here) ^""" ) @@ -667,13 +632,13 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (interpolator) 0ms - assertion failed (modules/framework-cats/shared/src/test/scala/Meta.scala:44) + assertion failed (modules/framework-cats/shared/src/test/scala/Meta.scala#L44) expect(x == "2") Use the `clue` function to troubleshoot - modules/framework-cats/shared/src/test/scala/Meta.scala:44 + modules/framework-cats/shared/src/test/scala/Meta.scala#L44 forEach(Option(s"$x"))(x => expect(x == "2")) ^""" ) @@ -681,13 +646,13 @@ object DogFoodTests extends IOSuite { assertInlineSnapshot( actual, """- (interpolator) 0ms - assertion failed (modules/framework-cats/shared/src/test/scala/Meta.scala:44) + assertion failed (modules/framework-cats/shared/src/test/scala/Meta.scala#L44) expect(x == "2") Use the `clue` function to troubleshoot - modules/framework-cats/shared/src/test/scala/Meta.scala:44 + modules/framework-cats/shared/src/test/scala/Meta.scala#L44 forEach(Option(s"$x"))(x => expect(x == "2")) ^""" ) diff --git a/modules/framework-cats/shared/src/test/scala/Meta.scala b/modules/framework-cats/shared/src/test/scala/Meta.scala index 1687d8fd..bf7c920b 100644 --- a/modules/framework-cats/shared/src/test/scala/Meta.scala +++ b/modules/framework-cats/shared/src/test/scala/Meta.scala @@ -45,14 +45,6 @@ object Meta { } } - object SourceUrlSuite extends SimpleIOSuite { - override protected def effectCompat: UnsafeRun[IO] = SourceUrlUnsafeRun - - test("(failure)") { - IO(expect.eql(1, 2)) - } - } - object MutableSuiteTest extends MutableSuiteTest object Boom extends Error("Boom") with scala.util.control.NoStackTrace @@ -338,7 +330,6 @@ object Meta { object SetTimeUnsafeRun extends CatsUnsafeRun { import scala.concurrent.duration._ - import cats.effect.std.Env private val setTimestamp = weaver.internals.Timestamp.localTime(12, 54, 35) @@ -347,26 +338,6 @@ object Meta { def monotonic: cats.effect.IO[FiniteDuration] = IO(0L.millis) def applicative: cats.Applicative[cats.effect.IO] = cats.Applicative[IO] } - - // Override the environment variables such that local error messages are displayed in CI runs. - override def env: Env[IO] = new Env[IO] { - def entries: IO[List[(String, String)]] = IO.pure(Nil) - def get(name: String): IO[Option[String]] = IO.pure(None) - } - } - - object SourceUrlUnsafeRun extends CatsUnsafeRun { - import cats.effect.std.Env - - override def clock: Clock[IO] = SetTimeUnsafeRun.clock - - override def env: Env[IO] = new Env[IO] { - def entries: IO[List[(String, String)]] = IO(List( - ("WEAVER_SOURCE_URL", - "https://github.com/typelevel/weaver-test/blob/v0.12.0/") - )) - def get(name: String): IO[Option[String]] = entries.map(_.toMap.get(name)) - } } } diff --git a/modules/framework-cats/shared/src/test/scala/TagDogFoodTests.scala b/modules/framework-cats/shared/src/test/scala/TagDogFoodTests.scala index 4ab1e43a..8ffd555a 100644 --- a/modules/framework-cats/shared/src/test/scala/TagDogFoodTests.scala +++ b/modules/framework-cats/shared/src/test/scala/TagDogFoodTests.scala @@ -28,9 +28,9 @@ object TagDogFoodTests extends IOSuite { failureMessages, List( s"""- (should-fail) 0ms - | 'Only' tag is not allowed when `isCI=true` (src/main/MaoTests.scala:1)""".stripMargin, + | 'Only' tag is not allowed when `isCI=true` (src/main/MaoTests.scala#L1)""".stripMargin, s"""- (should-also-fail) 0ms - | 'Only' tag is not allowed when `isCI=true` (src/main/MaoTests.scala:1)""".stripMargin + | 'Only' tag is not allowed when `isCI=true` (src/main/MaoTests.scala#L1)""".stripMargin ) ) } diff --git a/modules/framework/js-native/src/main/scala/RunnerCompat.scala b/modules/framework/js-native/src/main/scala/RunnerCompat.scala index 99100aed..a1c79d64 100644 --- a/modules/framework/js-native/src/main/scala/RunnerCompat.scala +++ b/modules/framework/js-native/src/main/scala/RunnerCompat.scala @@ -131,7 +131,7 @@ trait RunnerCompat[F[_]] { self: sbt.testing.Runner => val outcome = TestOutcome("Unexpected failure", 0.seconds, - Result.from(sourceLocationUrl = None, error), + Result.from(error), Chain.empty) reportTest(outcome).productR( reportDoneF(TestOutcomeNative.from(fqn)(outcome))) diff --git a/modules/framework/jvm/src/main/scala/RunnerCompat.scala b/modules/framework/jvm/src/main/scala/RunnerCompat.scala index 2e9ceca5..cce720e6 100644 --- a/modules/framework/jvm/src/main/scala/RunnerCompat.scala +++ b/modules/framework/jvm/src/main/scala/RunnerCompat.scala @@ -233,7 +233,7 @@ trait RunnerCompat[F[_]] { self: sbt.testing.Runner => val outcome = TestOutcome("Unexpected failure", 0.seconds, - Result.from(sourceLocationUrl = None, error), + Result.from(error), Chain.empty) Async[F].guarantee(outcomes