Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@hmemcpy
Copy link
Contributor

@hmemcpy hmemcpy commented Sep 25, 2020

Fixes #3152

This PR adds source information of the currently executing test/testM, and storing it inside a test annotation.
This is done with a small macro, without any external libraries!

image

Result (with location rendering enabled):

+ Source location - location: "(/Users/hmemcpy/.../test/SourceLocationSpec.scala,5)"
  + Tests report their source location - location: "(/Users/hmemcpy/.../test/SourceLocationSpec.scala,5)"
Ran 1 test in 433 ms: 1 succeeded, 0 ignored, 0 failed

This functionality is required in order to provide external tooling support with better navigation to the location of the test, for instance, from IntelliJ's test runner window - double clicking on the test entry can take you to the file.
To achieve this, IntelliJ expects test runners to provide "location hints" in form of a filename:lineNumber URL. This URL can be constructed externally, but only if this information is available from ZIO Test.

I was, unfortunately, not able to extract the exact information I required from ZIO's stack traces.

I welcome a discussion/alternative implementation suggestions!

Related: #3911

@adamgfraser
Copy link
Contributor

@hmemcpy Thanks for working on this!

Conceptually I feel like our tracing functionality should be supporting this and if it is not then we need either need additional functionality in the tracing or we need to be using the existing functionality better in ZIO Test. I am cognizant that this is a feature we have wanted to support for a while and so we could potentially add with the understanding that we are going to work to implement ourselves and remove the dependency. But since being zero dependency is one of our selling points I am a little hesitant about adding a dependency we don't think we ultimately are going to need.

What issues did you run into trying to use the traces for this? I can look at it too.

@hmemcpy
Copy link
Contributor Author

hmemcpy commented Sep 25, 2020

Thanks, @adamgfraser, for your comments!

Admittedly, I haven't looked in a while at the current state of tracing, but as I recall from my last attempt, I wasn't quite able to extract the correct line number of the calling test. I will have to look again!

Mainly, I really want this functionality added because I keep finding myself double-clicking on the test results in IntelliJ and NOTHING HAPPENS! And unfortunately, that part is not very extensible so I can't work around it on the plugin side...

I will try again with using traces. However, what do you think about storing this information in an annotation? Particularly, it seems this would be an "internal" annotation which doesn't get rendered.

@adamgfraser
Copy link
Contributor

I think using an annotation for this is fantastic. I think if we implemented it with tracing we would potentially implement it with an annotation anyway so I think it is more a question of how we get the information in the first place than how we store it.

@neko-kai
Copy link
Member

neko-kai commented Sep 28, 2020

Given

test("name")(<by-name IO of type `=> ZIO[...]`, which is `Function0` at runtime>)

pass the Function0 to zio.internal.stacktracer.Tracer#trace - this will give you the source location of the given lambda. You can get the current Tracer implementation from Runtime[_]#platform.tracing.tracer

But this won't work on Scala.js though (the entirety of tracing doesn't).
I'm actually somewhat more keen on capturing source position in compile-time, though to avoid dependencies you may just copy-paste an optimized implementation instead of depending on sourcecode (but you'll need to write a dotty version, AFAIK sourcecode is already ported)

package izumi.fundamentals.platform.language

final case class SourceFilePosition(file: String, line: Int) {
  override def toString: String = s"($file:$line)" // It will be a clickable link in intellij console
}

object SourceFilePosition {
  def unknown = SourceFilePosition("?", 0)
}

package izumi.fundamentals.platform.language

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

final case class SourceFilePositionMaterializer(get: SourceFilePosition) extends AnyVal

object SourceFilePositionMaterializer {
  @inline def sourcePosition(implicit ev: SourceFilePositionMaterializer): SourceFilePosition = ev.get

  implicit def materialize: SourceFilePositionMaterializer = macro SourcePositionMaterializerMacro.getSourceFilePositionMaterializer

  object SourcePositionMaterializerMacro {

    // scalactic.source.Position does all that manually and uses `setType`s to avoid retypechecking, oh well might as well...
    // PS: To ensure that retypechecking does not happen, put a known bad type into `setType` and watch compiler crash.
    //     If it _doesn't_ crash, that means one of the tree nodes was missing a `setType`

    def literal(c: blackbox.Context)(tpe: c.Type)(a: Any): c.universe.Literal = {
      c.internal.setType(c.universe.Literal(c.universe.Constant(a)), tpe)
    }

    def getSourceFilePositionMaterializer(c: blackbox.Context): c.Tree = {
      import c.universe._
      import c.universe.internal._
      import c.universe.internal.gen.{mkAttributedIdent, mkAttributedSelect}

      val sourceFilePosition = getSourceFilePosition(c)

      val matTpe = typeOf[SourceFilePositionMaterializer]
      val matModule = matTpe.companion.typeSymbol.asClass.module

      val sourceFilePositionMaterliazer = Apply(
        mkAttributedSelect(
          mkAttributedIdent(matModule),
          matModule.typeSignature.decl(TermName("apply")),
        ),
        sourceFilePosition :: Nil,
      )
      setType(sourceFilePositionMaterliazer, matTpe)
    }

    def getSourceFilePosition(c: blackbox.Context): c.Tree = {
      import c.universe._
      import c.universe.internal._
      import c.universe.internal.gen.{mkAttributedIdent, mkAttributedSelect}

      val posTpe = typeOf[SourceFilePosition]
      val posModule = posTpe.companion.typeSymbol.asClass.module

      val sourceFilePosition = Apply(
        mkAttributedSelect(
          mkAttributedIdent(posModule),
          posModule.typeSignature.decl(TermName("apply")),
        ),
        literal(c)(definitions.StringClass.toTypeConstructor)(c.enclosingPosition.source.file.name) ::
        literal(c)(definitions.IntTpe)(c.enclosingPosition.line) :: Nil,
      )
      setType(sourceFilePosition, posTpe)
    }

  }

}

@hmemcpy
Copy link
Contributor Author

hmemcpy commented Nov 11, 2020

Sorry for the lack of progress on this. I'd like to give this a serious go during the hackathon.

@hmemcpy hmemcpy marked this pull request as ready for review November 21, 2020 15:31
@hmemcpy
Copy link
Contributor Author

hmemcpy commented Nov 21, 2020

Alright. After taking a good look the problem turned out to be very simple. One tiny macro to capture the info. I implemented two variants - one for dotty and one for Scala 2.x.

Finally, I had an issue when trying to test this. I wanted to do something like:

testM("captures source info") { 
  for { 
      a <- Annotations.get(TestAnnotation.location)
      loc = a.head
  yield assert(loc)(equalTo("...."))
}

however Annotations.get always returned an empty list for me. I'm not sure if/how to access the annotations map of the currently running spec...

Other than that - it solves the problem and opens the possibility to create richer test and error reporting (see munit, for example), with tooling support for free.

@hmemcpy hmemcpy force-pushed the test-location branch 3 times, most recently from bcf4ac3 to f693570 Compare November 21, 2020 16:49
@hmemcpy hmemcpy marked this pull request as draft November 26, 2020 12:48
@hmemcpy hmemcpy force-pushed the test-location branch 5 times, most recently from 45c2804 to 275ecd3 Compare November 28, 2020 20:07
@hmemcpy
Copy link
Contributor Author

hmemcpy commented Nov 28, 2020

Well... seems that I am blocked by 2 bugs I found in dotty... both have to do with a combination of using by-name parameters and defining traits in them. I reported them to dotty, will see if I have a suitable workaround or fix...

@hmemcpy
Copy link
Contributor Author

hmemcpy commented Dec 2, 2020

So it looks like I'm blocked by two issues in dotty, and according to this comment by Odersky:

image

it's not going to be resolved soon. The problem is with any test code that defines sealed trait hierarchies inside the test body - dotty currently can't inline it properly. In ZIO tests there are two such tests, causing different crashes in the compiler, but the underlying cause is the same - both define sealed traits.

Instead of "working around" the crash by fixing the tests, I propose returning to the previous implementation of defining an implicit Location parameter that will capture the source location. I want to reiterate that it does not cause any changes in source compatibility, and being only used for tests - it's a fair assumption they are going to be recompiled anyway.

@adamgfraser, WDYT?

@hmemcpy hmemcpy force-pushed the test-location branch 5 times, most recently from 87b9a50 to bdd061d Compare December 7, 2020 07:56
@hmemcpy hmemcpy marked this pull request as ready for review December 7, 2020 07:56
@adamgfraser
Copy link
Contributor

@hmemcpy Too bad we take a step back on this with Scala 3 but I think it is time to get this in. I agree with your approach.

@hmemcpy hmemcpy force-pushed the test-location branch 2 times, most recently from 66639a0 to e2081c7 Compare December 25, 2020 12:47
@hmemcpy
Copy link
Contributor Author

hmemcpy commented Dec 25, 2020

I rebased on top of the latest changes in dotty, everything is passing again!

@adamgfraser, could we get this merged?

@hmemcpy
Copy link
Contributor Author

hmemcpy commented Jan 16, 2021

I finally rebased it again on top of all the other test-related changes. Made a few tweaks/renames to the layout of the dotty variants to make it consistent with Scala 2 (mainly, extracted Macros into its own file, renamed the proxy methods to be consistent).

Also added capturing source location in the new MutableSpecs.

Let's hope this works this time :)

@hmemcpy hmemcpy force-pushed the test-location branch 3 times, most recently from d31422f to cc81055 Compare January 16, 2021 22:04
@jdegoes jdegoes merged commit d6f0580 into zio:master Jan 19, 2021
@hmemcpy
Copy link
Contributor Author

hmemcpy commented Jan 19, 2021

Thank you @jdegoes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ZIO Test: include source location traces for all test executions

4 participants