Skip to content

Commit

Permalink
Fix ScalaMock for SJS 1.x (#362)
Browse files Browse the repository at this point in the history
* Add .bsp to gitignore

* Enable scalatest for shared / JS

* sbt 1.4.3

* Fix SJS test linking

* Include scalajs-stubs

* JSExport PoC

* Simplified solution without scalajs-stubs

* Revert cosmetic changes

* Cleanup MockMaker warning
  • Loading branch information
ddworak authored Nov 22, 2020
1 parent 335336a commit ff3e26d
Show file tree
Hide file tree
Showing 15 changed files with 40 additions and 72 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ target/
.settings/
*.sublime-workspace
sonatype.sbt
.bsp/
16 changes: 7 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ scalaVersion in ThisBuild := "2.11.12"
crossScalaVersions in ThisBuild := Seq("2.11.12", "2.12.12", "2.13.3")
//scalaJSUseRhino in ThisBuild := true

lazy val scalatest = "org.scalatest" %% "scalatest" % "3.2.2"
lazy val specs2 = "org.specs2" %% "specs2-core" % "4.10.5"
lazy val scalameta = "org.scalameta" %% "scalameta" % "4.3.24"
lazy val scalatest = Def.setting("org.scalatest" %%% "scalatest" % "3.2.2")
lazy val specs2 = Def.setting("org.specs2" %%% "specs2-core" % "4.10.5")

val commonSettings = Defaults.coreDefaultSettings ++ Seq(
unmanagedSourceDirectories in Compile ++= {
Expand All @@ -27,13 +26,12 @@ lazy val scalamock = crossProject(JSPlatform, JVMPlatform) in file(".") settings
publishArtifact in (Compile, packageDoc) := true,
publishArtifact in (Compile, packageSrc) := true,
publishArtifact in Test := false,
scalacOptions in (Compile, doc) ++= Opts.doc.title("ScalaMock") ++
scalacOptions in (Compile, doc) ++= Opts.doc.title("ScalaMock") ++
Opts.doc.version(version.value) ++ Seq("-doc-root-content", "rootdoc.txt", "-version"),
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value,
scalameta,
scalatest % Optional,
specs2 % Optional
scalatest.value % Optional,
specs2.value % Optional
)
)

Expand All @@ -45,7 +43,7 @@ lazy val examples = project in file("examples") settings(
name := "ScalaMock Examples",
skip in publish := true,
libraryDependencies ++= Seq(
scalatest % Test,
specs2 % Test
scalatest.value % Test,
specs2.value % Test
)
) dependsOn scalamock.jvm
57 changes: 7 additions & 50 deletions js/src/main/scala/org/scalamock/clazz/MockFunctionFinderImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,66 +29,23 @@ object MockFunctionFinderImpl {
def mockedFunctionGetter[M: c.WeakTypeTag](c: Context)
(obj: c.Tree, name: c.Name, targs: List[c.Type], actuals: List[c.universe.Type]): c.Expr[M] = {
import c.universe._
val utils = new MacroUtils[c.type](c)
import utils._

def hasValueTypeArgs(baseSymbol: Symbol, owner: Type): Boolean = {
val baseType = owner.baseType(baseSymbol)
baseType.typeArgs.nonEmpty // && baseType.typeArgs.forall(_ <:< typeOf[AnyVal])
}

// this somehow replicates postfix logic from scala-js, there have to be a better way
def privateSuffix(owner: Tree): String = {
val objectSymbol = c.typeOf[Object].typeSymbol

val tpe = owner.tpe

val baseNonInterfaceParentCount = tpe.baseClasses.count( symbol =>
symbol != objectSymbol && (symbol.isClass && !symbol.asClass.isTrait)
)

// this was found in experiments and should be considered as magical cosmological constant
val haveBaseClassWithValTypeArgs = tpe.typeSymbol.owner != null && tpe.typeSymbol.owner.isPackage &&
tpe.baseClasses.exists(hasValueTypeArgs(_, owner.tpe))

val idx = baseNonInterfaceParentCount + (if (haveBaseClassWithValTypeArgs) 1 else 0)
"$" + idx.toString
}

def encodeMemberNameInternal(s: String): String = {
s.replace("_", "$und")
}

// def printTypeSymbol(t: Type): String = {
// t.toString + "[" + t.baseClasses.map( base ⇒
// t.baseType(base).toString
// ).mkString(",") + "]"
// }

def mockFunctionName(name: Name, t: Type, targs: List[Type]) = {
val method = t.member(name).asTerm
val nameStr = encodeMemberNameInternal(name.toString)

if (method.isOverloaded)
"mock$" + nameStr + "$" + method.alternatives.indexOf(MockFunctionFinder.resolveOverloaded(c)(method, targs, actuals))
"mock$" + name + "$" + method.alternatives.indexOf(MockFunctionFinder.resolveOverloaded(c)(method, targs, actuals))
else
"mock$" + nameStr + "$0"
"mock$" + name + "$0"
}

val fullName = mockFunctionName(name, obj.tpe, targs) + privateSuffix(obj)
val fld = freshTerm("fld")
val fullName = TermName(mockFunctionName(name, obj.tpe, targs))

val code = c.Expr[M](q"""{
val code = c.Expr[M](
q"""{
import scala.scalajs.js
val $fld = js.Object.getOwnPropertyDescriptor($obj.asInstanceOf[js.Object], $fullName)
if (js.isUndefined($fld)) {
throw new IllegalArgumentException("Property '" + $fullName + "' is not defined in '" + $obj + "'. Available properties: " +
js.Object.getOwnPropertyNames($obj.asInstanceOf[js.Object]).mkString(",")
)
}
$fld.value.asInstanceOf[${weakTypeOf[M]}]
$obj.asInstanceOf[js.Dynamic].$fullName.asInstanceOf[${weakTypeOf[M]}]
}""")
// println(code)

code
}
}
12 changes: 8 additions & 4 deletions shared/src/main/scala/org/scalamock/clazz/MockMaker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,17 @@ class MockMaker[C <: Context](val ctx: C) {
val clazz = classType(paramCount(mt))
val types = (paramTypes(mt) map mockParamType _) :+ mockParamType(finalResultType(mt))
val name = applyOn(scalaSymbol, "apply", mockNameGenerator.generateMockMethodName(m, mt))

ValDef(Modifiers(),
val termName = mockFunctionName(m)
val additionalAnnotations = if(isScalaJs) List(jsExport(termName.encodedName.toString)) else Nil
ValDef(
Modifiers().mapAnnotations(additionalAnnotations ::: _),
mockFunctionName(m),
AppliedTypeTree(Ident(clazz.typeSymbol), types), // see issue #24
callConstructor(
New(AppliedTypeTree(Ident(clazz.typeSymbol), types)),
mockContext.tree, name))
mockContext.tree, name
)
)
}

// def <init>() = super.<init>()
Expand All @@ -217,7 +221,7 @@ class MockMaker[C <: Context](val ctx: C) {

val constructorArgumentsTypes = primaryConstructorOpt.map { constructor =>
val constructorTypeContext = constructor.typeSignatureIn(typeToMock)
val constructorArguments = constructor.paramss //constructorTypeContext.paramLists
val constructorArguments = constructor.paramLists
constructorArguments.map {
symbols => symbols.map(_.typeSignatureIn(constructorTypeContext))
}
Expand Down
8 changes: 5 additions & 3 deletions shared/src/main/scala/org/scalamock/util/MacroAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@

package org.scalamock.util

class MacroAdapter[C <: MacroAdapter.Context](val ctx2: C) {
import ctx2.universe._
trait MacroAdapter {

def freshTerm(prefix: String): TermName = ctx2.freshName(TermName(prefix))
protected val ctx: MacroAdapter.Context
import ctx.universe._

def freshTerm(prefix: String): TermName = ctx.freshName(TermName(prefix))
def internalTypeRef(pre: Type, sym: Symbol, args: List[Type]) = internal.typeRef(pre, sym, args)
def internalSuperType(thistpe: Type, supertpe: Type): Type = internal.superType(thistpe, supertpe)
def internalThisType(thistpe: Symbol) = internal.thisType(thistpe)
Expand Down
13 changes: 9 additions & 4 deletions shared/src/main/scala/org/scalamock/util/MacroUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ import MacroAdapter.Context
/**
* Helper functions to work with Scala macros and to create scala.reflect Trees.
*/
private[scalamock] class MacroUtils[C <: Context](ctx: C) extends MacroAdapter[C](ctx) { // ctx2 to avoid clash with ctx in MockMaker (eugh!)
import ctx2.universe._
private[scalamock] class MacroUtils[C <: Context](protected val ctx: C) extends MacroAdapter {
import ctx.universe._

final lazy val isScalaJs =
ctx.compilerSettings.exists(o => o.startsWith("-Xplugin:") && o.contains("scalajs-compiler"))

def jsExport(name: String) = q"new _root_.scala.scalajs.js.annotation.JSExport($name)"

// Convert a methodType into its ultimate result type
// For nullary and normal methods, this is just the result type
Expand Down Expand Up @@ -77,7 +82,7 @@ private[scalamock] class MacroUtils[C <: Context](ctx: C) extends MacroAdapter[C
def reportError(message: String) = {
// Report with both info and abort so that the user still sees something, even if this is within an
// implicit conversion (see https://issues.scala-lang.org/browse/SI-5902)
ctx2.info(ctx2.enclosingPosition, message, true)
ctx2.abort(ctx2.enclosingPosition, message)
ctx.info(ctx.enclosingPosition, message, true)
ctx.abort(ctx.enclosingPosition, message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.scalatest.events.{Event, TestFailed}
import org.scalatest.matchers.should.Matchers

import scala.language.postfixOps
import scala.reflect.ClassTag

trait TestSuiteRunner { this: Matchers =>

Expand All @@ -39,15 +40,15 @@ trait TestSuiteRunner { this: Matchers =>
reporter.lastEvent.get
}

def getThrowable[ExnT <: Throwable](event: Event)(implicit m: Manifest[ExnT]): ExnT = {
def getThrowable[ExnT <: Throwable : ClassTag](event: Event): ExnT = {
event shouldBe a[TestFailed]

val testCaseError = event.asInstanceOf[TestFailed].throwable.get
testCaseError shouldBe a[ExnT]
testCaseError.asInstanceOf[ExnT]
}

def getErrorMessage[ExnT <: Throwable](event: Event)(implicit m: Manifest[ExnT]): String = {
def getErrorMessage[ExnT <: Throwable : ClassTag](event: Event): String = {
getThrowable[ExnT](event).getMessage()
}
}

0 comments on commit ff3e26d

Please sign in to comment.