how to ignore test utility methods when scalatest detects failures? - scala

I have this convenience method in my tests:
def assertFormat[T: SexpFormat](start: T, expect: Sexp): Unit = {
val sexp = start.toSexp
assert(sexp === expect, s"${sexp.compactPrint} was not ${expect.compactPrint}")
expect.convertTo[T] should be(start)
}
which is basically a convenience for running an assertion pattern that I do a lot.
It's not possible to rewrite this as a Matcher because of the implicit requirement on SexpFormat[T] (although I'd be interested in hearing of ways to do this that don't require me to write the type MyFormat in foo should roundTrip[MyFormat](...))
If any tests fail inside this utility method, scalatest will flag the internals of assertFormat as being the cause of the test failure. But I really want scalatest to detect the caller of this method to be the cause of the test. How can I do that?
i.e. current output is
[info] - should support custom missing value rules *** FAILED ***
[info] SexpNil did not equal SexpCons(SexpSymbol(:duck),SexpCons(SexpNil,SexpNil)) nil was not (:duck nil) (FormatSpec.scala:11)
[info] org.scalatest.exceptions.TestFailedException:
[info] at org.scalatest.Assertions$class.newAssertionFailedException(Assertions.scala:529)
[info] at org.scalatest.FlatSpec.newAssertionFailedException(FlatSpec.scala:1691)
[info] at org.scalatest.Assertions$AssertionsHelper.macroAssert(Assertions.scala:502)
[info] at org.ensime.sexp.formats.FormatSpec$class.assertFormat(FormatSpec.scala:11)
[info] at org.ensime.sexp.formats.test.FamilyFormatsSpec.assertFormat(FamilyFormatsSpec.scala:151)
[info] at org.ensime.sexp.formats.test.FamilyFormatsSpec.roundtrip(FamilyFormatsSpec.scala:156)
[info] at org.ensime.sexp.formats.test.FamilyFormatsSpec$$anonfun$12.apply(FamilyFormatsSpec.scala:222)
[info] at org.ensime.sexp.formats.test.FamilyFormatsSpec$$anonfun$12.apply(FamilyFormatsSpec.scala:221)
FormatSpec.scala:11 is where my assertFormat is defined. The real failure is in FamilyFormatsSpec.scala:222 (which is calling another convenience method FamilyFormatsSpec.scala:156)

This is possible in ScalaTest 3.0 by taking an implicit org.scalactic.source.Position in your custom assertion. The position will then be computed (via a macro) whenever your assertFormat method is called, and that position will be picked up by the assert and matcher expression inside assertFormat. Here is how it would look:
import org.scalactic.source
def assertFormat[T: SexpFormat](start: T, expect: Sexp)(implicit pos: source.Position): Unit = {
val sexp = start.toSexp
assert(sexp === expect, s"${sexp.compactPrint} was not ${expect.compactPrint}")
expect.convertTo[T] should be(start)
}
The following example illstrates it. If you have ScalaTest 3.0 on the class path, just :load the following file into the Scala REPL:
:paste
import org.scalatest._
import org.scalactic._
import Matchers._
case class Sexp(o: Any) {
def compactPrint: String = o.toString
def convertTo[T: SexpFormat]: Sexp = implicitly[SexpFormat[T]].convertIt(o)
override def toString = "I'm too sexp for my shirt."
}
trait SexpFormat[T] {
def convertIt(o: Any): Sexp = new Sexp(o)
}
implicit class Sexpify(o: Any) {
def toSexp: Sexp = new Sexp(o)
}
implicit def universalSexpFormat[T]: SexpFormat[T] = new SexpFormat[T] {}
def assertFormat[T: SexpFormat](start: T, expect: Sexp): Unit = {
val sexp = start.toSexp
assert(sexp === expect, s"${sexp.compactPrint} was not ${expect.compactPrint}")
expect.convertTo[T] should be(start)
}
import org.scalatest.exceptions.TestFailedException
val before = intercept[TestFailedException] { assertFormat(1, new Sexp) }
println(s"${before.failedCodeStackDepth} - This stack depth points to the assert call inside assertFormat")
import org.scalactic.source
def betterAssertFormat[T: SexpFormat](start: T, expect: Sexp)(implicit pos: source.Position): Unit = {
val sexp = start.toSexp
assert(sexp === expect, s"${sexp.compactPrint} was not ${expect.compactPrint}")
expect.convertTo[T] should be(start)
}
val after = intercept[TestFailedException] { betterAssertFormat(1, new Sexp) }
println(s"${after.failedCodeStackDepth} - This stack depth is the betterAssertFormat call itself in your test code")
It will print:
3 - This stack depth points to the assert call inside assertFormat
4 - This stack depth is the betterAssertFormat call itself in your test code

Related

How can I override a class' method in a Scala 3 compiler plugin?

I want to auto-generate an overriden method in a compiler plugin (Scala 3) for a class like:
trait SpecialSerialize {
def toJson(sb: StringBuilder, c:SJConfig): Unit = {println("wrong")}
}
case class Person(name:String, age:Int) extends SpecialSerialize
The plugin would generate:
case class Person(name:String, age:Int) extends SpecialSerialize {
override def toJson(sb: StringBuilder, c:SJConfig): Unit = ... // code here
}
I have a phase:
class ReflectionWorkerPhase extends PluginPhase {
import tpd._
val phaseName = "reflectionWorker"
override val runsAfter = Set(Pickler.name)
override def transformTypeDef(tree: TypeDef)(implicit ctx: Context): Tree =
if tree.isClassDef && !tree.rhs.symbol.isStatic then // only look at classes
// 0. Get a FreshContext so we can set the tree to this tree. (for '{} later)
implicit val fresh = ctx.fresh
fresh.setTree(tree)
QuotesCache.init(fresh)
implicit val quotes:Quotes = QuotesImpl.apply() // picks up fresh
import quotes.reflect.*
// 1. Set up method symbol, define parameters and return type
val toJsonSymbol = Symbol.newMethod(
Symbol.spliceOwner,
"toJson",
MethodType(
List("sb","config"))( // parameter list
_ => List( // types of the parameters
TypeRepr.of[StringBuilder],
TypeRepr.of[SJConfig],
),
_ => TypeRepr.typeConstructorOf(classOf[Unit]) // return type
),
Flags.Override, // Note override here
Symbol.noSymbol
)
// 2. Get our class' Symbol for ownership reassignment
val classDef = tree.asInstanceOf[ClassDef]
val classSymbol = classDef.symbol
// 3. Define our method definition (DefDef) using our method symbol defined above
val toJsonMethodDef = DefDef(
toJsonSymbol,
{
case List(List(sb: Term, config: Term)) =>
given Quotes = toJsonSymbol.asQuotes
Some({
// Multiple quotes here intentional...
// Real code will generate a list of quoted statements
quoted.Expr.ofList(List(
'{ println("Hello") },
'{ println("World") }
))
}.asTerm.changeOwner(toJsonSymbol))
}
).changeOwner(classSymbol)
// 4. Add toJsonMethodDef to tree and return
val cd = ClassDef.copy(classDef)(
name = classDef.name,
constr = classDef.constructor,
parents = classDef.parents,
selfOpt = classDef.self,
body = toJsonMethodDef +: classDef.body
)
cd.asInstanceOf[dotty.tools.dotc.ast.tpd.Tree]
else
tree
}
When I use the plugin on a sample Person class, I get this error on compile:
Exception in thread "sbt-bg-threads-1" java.lang.ClassFormatError: Duplicate method name "toJson" with signature "(Lscala.collection.mutable.StringBuilder;Lco.blocke.scala_reflection.SJConfig;)V" in class file com/foo/Person
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1013)
at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
at java.base/java.net.URLClassLoader.defineClass(URLClassLoader.java:524)
at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:427)
at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:421)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
...
So something in the other compile phases failed to recognize my generated method as being an override and tried to copy in the "master" method from the trait, and of course the JVM lost its mind at runtime, finding 2 copies of toJson with the same signature.
How can I fix this so my generated method is recognized as a valid override such that a 2nd toJson method won't be generated in a later phase?

Scala 3 compiler plugin generating expected code but failing at runtime

I'm trying to get started with writing a compiler plugin for scala 3. At this stage, it's primarily based on https://github.com/liufengyun/scala3-plugin-example/blob/master/plugin/src/main/scala/Phases.scala (and the accompanying youtube video explaining how it works).
It's been an interesting process so far, and I'm getting a bit of a feel for some aspects of the compiler.
As a first step, I'm simply trying to wrap a method body into a block, print whatever the returned object was going to be, and then return the object.
This differs from the original plugin mainly in that there was a single side-effecting method call added to each method - this is also assigning a local variable, (which I think is probably the cause of the problem), and moving the method body into a block.
I've produced as minimal of a working example as I could in a fork here: https://github.com/robmwalsh/scala3-plugin-example
The plugin compiles fine, seems to run as part of compilation as expected, and then blows up at runtime. I'm not entirely sure if this is me doing something wrong (not unlikely) or a bug in the compiler (less likely, but a distinct possibility!).
Can anybody please shed some light on why this isn't working? I don't know what flags should be set when creating a new Symbol, so that's one possibility, but there's heaps of stuff that sorta seemed to work so I rolled with it.
Here's where I'm at (the interesting bits):
...
override def prepareForUnit(tree: Tree)(using ctx: Context): Context =
//find the printLn method
val predef = requiredModule("scala.Predef")
printlnSym = predef.requiredMethod("println", List(defn.AnyType))
ctx
override def transformDefDef(tree: DefDef)(using ctx: Context): Tree =
val sym = tree.symbol
// ignore abstract and synthetic methods
if tree.rhs.isEmpty|| sym.isOneOf(Synthetic | Deferred | Private | Accessor)
then return tree
try {
println("\n\n\n\n")
println("========================== tree ==========================")
println(tree.show)
// val body = {tree.rhs}
val body = ValDef(
newSymbol(
tree.symbol, termName("body"), tree.symbol.flags, tree.rhs.tpe),
Block(Nil, tree.rhs)
)
// println(body)
val bodyRef = ref(body.symbol)
val printRes = ref(printlnSym).appliedTo(bodyRef)
// shove it all together in a block
val rhs1 = tpd.Block(body :: printRes :: Nil, bodyRef)
//replace RHS with new
val newDefDef = cpy.DefDef(tree)(rhs = rhs1)
println("====================== transformed ======================")
println(newDefDef.show)
newDefDef
} catch {
case e =>
println("====================== error ===========================")
println(e)
println(e.printStackTrace)
tree
}
...
test program for compiler plugin
object Test extends App:
def foo: String = "forty two"
def bar(x: String): Int = x.length
def baz(x: String, y: Int): String = x + y
baz(foo, bar(foo))
output during compile using plugin (exactly what I wanted! I got very excited at this point)
========================== tree ==========================
def foo: String = "forty two"
====================== transformed ======================
def foo: String =
{
val body: ("forty two" : String) =
{
"forty two"
}
println(body)
body
}
========================== tree ==========================
def bar(x: String): Int = x.length()
====================== transformed ======================
def bar(x: String): Int =
{
val body: Int =
{
x.length()
}
println(body)
body
}
========================== tree ==========================
def baz(x: String, y: Int): String = x.+(y)
====================== transformed ======================
def baz(x: String, y: Int): String =
{
val body: String =
{
x.+(y)
}
println(body)
body
}
output during runtime :'( (this changes depending on the code it's running on, but always the same theme)
Exception in thread "main" java.lang.VerifyError: Bad local variable type
Exception Details:
Location:
testing/Test$.body$2()I #0: aload_1
Reason:
Type top (current frame, locals[1]) is not assignable to reference type
Current Frame:
bci: #0
flags: { }
locals: { 'testing/Test$' }
stack: { }
Bytecode:
0000000: 2bb6 007d ac
at testing.Test.main(Example.scala)
Edit: I'm using scala 3.1.2
I was using the existing flags when creating my new symbol. Instead, I needed to use the Local flag, which I suppose makes sense.

How to narrow down ZIO Schedule environment?

There is an example of a simple API that uses ZIO effect to return None or Option[String]. I use ZIO Schedule to run the effect as long the None is returned, but limited to a certain number of times. The example is based on the code from ZIO usecases_scheduling:
import zio._
import zio.random._
import zio.duration._
import zio.console.{Console, putStrLn}
import zio.Schedule
import scala.util.{Random => ScalaUtilRandom}
object RecordAPI {
def randomId(length: Int): String =
LazyList.continually(ScalaUtilRandom.nextPrintableChar).filter(_.isLetterOrDigit).take(length).mkString
def getRecordId: Task[Option[String]] = Task.effect(
if (ScalaUtilRandom.nextInt(10) >= 7) Some(randomId(16)) else None
)
}
object ScheduleUtil {
def schedule[A]: Schedule[Random, Option[String], Option[String]] =
(Schedule.exponential(10.milliseconds) && Schedule.recurs(10)) *> Schedule.recurWhile(_.isEmpty)
}
object RandomScheduler extends scala.App {
implicit val rt: Runtime[zio.ZEnv] = Runtime.default
rt.unsafeRun {
RecordAPI.getRecordId
.repeat(ScheduleUtil.schedule)
.foldM(
ex => putStrLn(s"failed with ${ex.getMessage}"),
success => putStrLn(s"Succeeded with $success")
)
}
}
This effect below has the type ZIO[Random with clock.Clock, Throwable, Option[String]]:
RecordAPI.getRecordId.repeat(ScheduleUtil.schedule)
I would like to remove the ScheduleUtil.schedule dependency on Random by providing the Random env and to receive the effect ZIO[Any with clock.Clock, Throwable, Option[String]]:
RecordAPI.getRecordId.repeat(ScheduleUtil.schedule.provide(Random))
but I get compilation error:
[error] found : zio.random.Random.type
[error] required: zio.random.Random
[error] (which expands to) zio.Has[zio.random.Random.Service]
[error] .repeat(ScheduleUtil.schedule.provide(Random))
[error] ^
[error] one error found
What parameter should be provided to the .provide method?
Error message talks you that you tries to pass to function provide Random.type
in the line:
RecordAPI.getRecordId.repeat(ScheduleUtil.schedule.provide(Random))
Random is passed as type but provide expects instance of Random. So you can make your code compilable just replacing Random type to some it's instance:
val hasRandomService: Random = Has.apply(Random.Service.live)
val randomIdZIO: ZIO[Random, Throwable, Option[String]] =
RecordAPI.getRecordId.repeat(ScheduleUtil.schedule.provide(hasRandomService))
but if you want to get rid of ScheduleUtil.schedule maybe it's better to use Schedule.fromFunction function:
val randomIdZIOFromFunction: ZIO[Random, Throwable, Option[String]] =
RecordAPI.getRecordId.repeat(
Schedule.fromFunction(_ => if (ScalaUtilRandom.nextInt(10) >= 7) Some(randomId(16)) else None)
)

Node based Iterable implementation in Scala

Continuing my travel through exercises from Sedgwick and Wayne's Algorithms I came across one in which I have to implement a RandomBag. Originally RandomBag is supposed to implement Iterable (in java) and its Iterator had to serve items in random order.
This is the companion object of my ImmutableRandomBag:
object ImmutableRandomBag{
case class Node[Item](item: Item, next: Option[Node[Item]])
def apply[Item](maybeNode: Option[Node[Item]], size: Int): ImmutableRandomBag[Item] = new ImmutableRandomBag(maybeNode, size)
}
And this is beginning of the class itself:
class ImmutableRandomBag[Item](maybeNode: Option[Node[Item]], size: Int) extends Iterable[Item]{
override def isEmpty: Boolean = size == 0
def add(item: Item) = {
ImmutableRandomBag(Some(Node(item, maybeNode)), size +1)
}
...
}
My understanding was that the val size should have overridden the def size from Iterable trait. When testing the add method I am getting the IndexOutOfBounException:
class RandomBagSpec extends BaseSpec {
trait RandomBag{
val begin = new ImmutableRandomBag[Connection](None, 0)
}
...
"Adding an item to empty RandomBag" should "return another bag with size 1" in new RandomBag {
val bag = begin.add(Connection(0,1))
bag.size should equal(1)
}
}
While debugging size is correctly evaluated in the constructor parameter, so I am not sure where the IndexOutOfBoundException comes from, but I get it whenever I call the add method. Maybe the problem sprouts from the following. In ImmutableRandomBag there's also Iterator implementation:
...
override def iterator: Iterator[Item] = new RandomIterator[Item](maybeNode)
private class RandomIterator[Item](first: Option[Node[Item]]) extends Iterator[Item]{
first match {
case Some(node) => random(node)
case None =>
}
var current: Int = 0
var container: Vector[Item] = Vector()
override def hasNext: Boolean = current < ImmutableRandomBag.this.size
override def next(): Item = {
val item = container(current)
current += 1
item
}
def random(first: Node[Item]) = {
#tailrec
def randomHelper(next: Option[Node[Item]], acc: List[Item]):List[Item]= next match {
case None => acc
case Some(node) => randomHelper(node.next, node.item::acc)
}
val items = randomHelper(Some(first), List[Item]())
container = Random.shuffle(items).toVector
}
}
}
And I have a different test in the same spec for it:
...
"Random Bag's iterator" should "contain all items passed to parent iterable" in new RandomBag{
val connections = List(Connection(0,1), Connection(1,0), Connection(1,1))
var localRB = begin
for(c <- connections) localRB = localRB.add(c)
assert(localRB.iterator.forall(conn=> connections.contains(conn)) == true)
}
...
I also get an IndexOutOfBoundException with the following stack:
[info] RandomBagSpec:
[info] Random Bag's iterator
[info] - should contain all items passed to parent iterable *** FAILED ***
[info] java.lang.IndexOutOfBoundsException: 0
[info] at scala.collection.immutable.Vector.checkRangeConvert(Vector.scala:123)
[info] at scala.collection.immutable.Vector.apply(Vector.scala:114)
[info] at ca.vgorcinschi.algorithms1_3_34.ImmutableRandomBag$RandomIterator.next(ImmutableRandomBag.scala:31)
[info] at scala.collection.Iterator.forall(Iterator.scala:956)
[info] at scala.collection.Iterator.forall$(Iterator.scala:954)
[info] at ca.vgorcinschi.algorithms1_3_34.ImmutableRandomBag$RandomIterator.forall(ImmutableRandomBag.scala:18)
[info] at ca.vgorcinschi.algorithms1_5_19.RandomBagSpec$$anon$1.<init>(RandomBagSpec.scala:16)
[info] at ca.vgorcinschi.algorithms1_5_19.RandomBagSpec.$anonfun$new$1(RandomBagSpec.scala:12)
[info] at org.scalatest.OutcomeOf.outcomeOf(OutcomeOf.scala:85)
[info] at org.scalatest.OutcomeOf.outcomeOf$(OutcomeOf.scala:83)
The issue seems to come from calling Iterator's next method and indeed the container Vector doesn't contain any elements:
but why is next being called before random?
val size should have overridden the def size from Iterable trait
A val would have, but you don't have one; you just have a constructor parameter in a non-case class. Effectively it's a private val and can't override anything.
but why is next being called before random?
It isn't; in RandomIterator's constructor, random is called (as part of first match ...) before the initializer container = Vector(). next is called only after constructor.

Is there an equivalent to SuppressWarnings in Scala?

I was wondering if scala had an equivalent to java's #SuppressWarnings that can be applied to a function or whatever to ignore any deprecation warnings[1] that function emits?
1: Relevant warning in my case is: method stop in class Thread is deprecated: see corresponding Javadoc for more information. I am aware of the problems with stop however there are still some cases where due to legacy code we have to use it.
No, and an enhancement request [1] for such a feature was closed as wontfix.
I agree it would be useful. I expect that the Scala core team aren't against the idea, but they have finite resources and many higher priorities.
update: this feature was eventually implemented in scala 2.13.2 release on 2020-04-22, see this answer
[1] https://issues.scala-lang.org/browse/SI-1781
EDIT: You should use #nowarn
There is a simple compiler plugin for this: silencer (a bit shameless plug)
Scala 2.13.2 provides #nowarn annotation developed on the basis of ghik's silencer, for example
import scala.annotation.nowarn
def t = { 0: #nowarn; 1 }
raises no warnings, whilst
def t = { 0; 1 }
gives
warning: a pure expression does nothing in statement position; multiline expressions might require enclosing parentheses
def t = { 0; 1 }
^
Here is how to suppress all warnings in sbt:
import sbt._
import Keys._
import KeyRanks.DTask
import xsbti.{Reporter, Problem, Position, Severity}
private lazy val compilerReporter = TaskKey[xsbti.Reporter](
"compilerReporter",
"Experimental hook to listen (or send) compilation failure messages.",
DTask
)
val ignoreWarnings = Seq(
compilerReporter in (Compile, compile) :=
new xsbti.Reporter {
private val buffer = collection.mutable.ArrayBuffer.empty[Problem]
def reset(): Unit = buffer.clear()
def hasErrors: Boolean = buffer.exists(_.severity == Severity.Error)
def hasWarnings: Boolean = buffer.exists(_.severity == Severity.Warn)
def printSummary(): Unit = {
print("\033c")
if (problems.nonEmpty) {
problems.foreach{ p =>
println("=====================================================")
println(p.position)
println(p.message)
println()
println()
}
}
}
def problems: Array[Problem] = buffer.toArray
def log(problem: Problem): Unit = {
if (problem.severity == Severity.Error) {
buffer.append(problem)
}
}
def log(pos: Position, msg: String, sev: Severity): Unit = {
log(new Problem {
def category: String = "foo"
def severity: Severity = sev
def message: String = msg
def position: Position = pos
})
}
def comment(pos: xsbti.Position, msg: String): Unit = ()
}
)