Skip to content
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ scalaVersion in ThisBuild := "2.11.6"

crossScalaVersions in ThisBuild := Seq("2.10.5", "2.11.6")

parallelExecution in Test := false
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabled parallel execution because I encounter some weird exception without this line, e.g.,

[info] - checkScalaMethodPresenceVerbose *** FAILED ***
[info]   java.lang.ClassNotFoundException: o .lang.scala.Observ
[info]   at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
[info]   at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
[info]   at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
[info]   at java.lang.Class.forName0(Native Method)
[info]   at java.lang.Class.forName(Class.java:348)
[info]   at scala.reflect.runtime.JavaMirrors$JavaMirror.javaClass(JavaMirrors.scala:500)
[info]   at scala.reflect.runtime.SymbolLoaders$TopClassCompleter.complete(SymbolLoaders.scala:32)
[info]   at scala.reflect.internal.Symbols$Symbol.info(Symbols.scala:1231)
[info]   at scala.reflect.internal.Types$TypeRef.thisInfo(Types.scala:2407)
[info]   at scala.reflect.internal.Types$TypeRef.baseClasses(Types.scala:2412)

I guess some reflection APIs are not thread-safe (or a bug?). So if running multiple CompletenessKit at the same time, it will crash.

However, since RxScala's unit tests are pretty fast, disabling it hurts nothing.


libraryDependencies ++= Seq(
"io.reactivex" % "rxjava" % "1.0.12",
"org.mockito" % "mockito-core" % "1.9.5" % "test",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class BlockingObservable[+T] private[scala] (val o: Observable[T])
}

/**
* Returns an `Iterator` that iterates over all items emitted by this [[Observable]].
* Returns an `Iterable` that iterates over all items emitted by this [[Observable]].
*/
def toIterable: Iterable[T] = {
asJava.toIterable.asScala: Iterable[T] // useless ascription because of compiler bug
Expand Down
494 changes: 0 additions & 494 deletions src/test/scala/rx/lang/scala/CompletenessTest.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rx.lang.scala.completeness

import scala.reflect.runtime.universe.typeOf

class BlockingObservableCompletenessKit extends CompletenessKit {
override def rxJavaType = typeOf[rx.observables.BlockingObservable[_]]

override def rxScalaType = typeOf[rx.lang.scala.observables.BlockingObservable[_]]

override protected def correspondenceChanges = Map(
"first(Func1[_ >: T, Boolean])" -> "[use `Observable.filter(p).toBlocking.head`]",
"firstOrDefault(T)" -> "headOrElse(=> U)",
"firstOrDefault(T, Func1[_ >: T, Boolean])" -> "[use `Observable.filter(p).toBlocking.headOrElse(=> U)`]",
"forEach(Action1[_ >: T])" -> "foreach(T => Unit)",
"from(Observable[_ <: T])" -> "[use `Observable.toBlocking`]",
"last(Func1[_ >: T, Boolean])" -> "[use `Observable.filter(p).toBlocking.last`]",
"lastOrDefault(T)" -> "lastOrElse(=> U)",
"lastOrDefault(T, Func1[_ >: T, Boolean])" -> "[use `Observable.filter(p).toBlocking.lastOrElse(=> U)`]",
"mostRecent(T)" -> "mostRecent(U)",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also change the signature of RxScala's mostRecent and get rid of this line...

"single(Func1[_ >: T, Boolean])" -> "[use `Observable.filter(p).toBlocking.single`]",
"singleOrDefault(T)" -> "singleOrElse(=> U)",
"singleOrDefault(T, Func1[_ >: T, Boolean])" -> "[use `Observable.filter(p).toBlocking.singleOrElse(=> U)`]",
"getIterator()" -> "[use `toIterable.toIterator`]"
)
}
181 changes: 181 additions & 0 deletions src/test/scala/rx/lang/scala/completeness/CompletenessKit.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rx.lang.scala.completeness

import org.junit.Test
import org.scalatest.junit.JUnitSuite

import scala.collection.immutable.SortedMap
import scala.reflect.runtime.universe.{Symbol, Type, typeOf}

/**
* If adding a new [[CompletenessKit]], please also update [[CompletenessTables.completenessKits]] to generate its comparison table.
*/
trait CompletenessKit extends JUnitSuite {

/**
* Return the type of the Java class to check
*/
def rxJavaType: Type

/**
* Return the type of the Scala class to check
*/
def rxScalaType: Type

/**
* Manually added mappings from Java methods to Scala methods. Sometimes, it's hard to map some Java methods to Scala methods
* automatically. Use this one to create the mappings manually.
*/
protected def correspondenceChanges: Map[String, String]

/**
* Return all public Java instance and static methods
*/
final def rxJavaPublicInstanceAndCompanionMethods: Iterable[String] = getPublicInstanceAndCompanionMethods(rxJavaType)


/**
* Return all public Scala methods and companion methods.
*/
final def rxScalaPublicInstanceAndCompanionMethods: Iterable[String] = getPublicInstanceAndCompanionMethods(rxScalaType)

/**
* Maps each method from the Java class to its corresponding method in the Scala class
*/
final def correspondence = defaultMethodCorrespondence ++ correspondenceChanges // ++ overrides LHS with RHS

/**
* Creates default method correspondence mappings, assuming that Scala methods have the same
* name and the same argument types as in Java
*/
private def defaultMethodCorrespondence: Map[String, String] = {
val allMethods = getPublicInstanceAndCompanionMethods(rxJavaType)
val tuples = for (javaM <- allMethods) yield (javaM, javaMethodSignatureToScala(javaM))
tuples.toMap
}

private def removePackage(s: String) = s.replaceAll("(\\w+\\.)+(\\w+)", "$2")

private def methodMembersToMethodStrings(members: Iterable[Symbol]): Iterable[String] = {
for (member <- members; alt <- member.asTerm.alternatives) yield {
val m = alt.asMethod
// multiple parameter lists in case of curried functions
val paramListStrs = for (paramList <- m.paramss) yield {
paramList.map(
symb => removePackage(symb.typeSignature.toString.replaceAll(",(\\S)", ", $1"))
).mkString("(", ", ", ")")
}
val name = alt.asMethod.name.decodedName.toString
name + paramListStrs.mkString("")
}
}

private def getPublicInstanceMethods(tp: Type): Iterable[String] = {
val ignoredSuperTypes = Set(typeOf[AnyRef], typeOf[Any], typeOf[AnyVal], typeOf[Object])
val superTypes = tp.baseClasses.map(_.asType.toType).filter(!ignoredSuperTypes(_))
// declarations: => only those declared in
// members => also those of superclasses
methodMembersToMethodStrings(superTypes.flatMap(_.declarations).filter {
m =>
m.isMethod && m.isPublic &&
m.annotations.forall(_.toString != "java.lang.Deprecated") // don't check deprecated classes
})
// TODO how can we filter out instance methods which were put into companion because
// of extends AnyVal in a way which does not depend on implementation-chosen name '$extension'?
.filter(! _.contains("$extension"))
// `access$000` is public. How to distinguish it from others without hard-code?
.filter(! _.contains("access$000"))
// Ignore constructors
.filter(! _.startsWith("<init>"))
}

/**
* Return all public instance methods and companion methods of a type. Also applicable for Java types.
*/
private def getPublicInstanceAndCompanionMethods(tp: Type): Iterable[String] =
getPublicInstanceMethods(tp) ++
getPublicInstanceMethods(tp.typeSymbol.companionSymbol.typeSignature)

private def javaMethodSignatureToScala(s: String): String = {
s.replaceAllLiterally("Long, Long, TimeUnit", "Duration, Duration")
.replaceAllLiterally("Long, TimeUnit", "Duration")
.replaceAll("Action0", "() => Unit")
// nested [] can't be parsed with regex, so these will have to be added manually
.replaceAll("Action1\\[([^]]*)\\]", "$1 => Unit")
.replaceAll("Action2\\[([^]]*), ([^]]*)\\]", "($1, $2) => Unit")
.replaceAll("Func0\\[([^]]*)\\]", "() => $1")
.replaceAll("Func1\\[([^]]*), ([^]]*)\\]", "$1 => $2")
.replaceAll("Func2\\[([^]]*), ([^]]*), ([^]]*)\\]", "($1, $2) => $3")
.replaceAllLiterally("_ <: ", "")
.replaceAllLiterally("_ >: ", "")
.replaceAllLiterally("<repeated...>[T]", "T*")
.replaceAll("(\\w+)\\(\\)", "$1")
}

@Test
def checkScalaMethodPresenceVerbose(): Unit = {
println("\nTesting that all mentioned Scala methods exist")
println( "----------------------------------------------\n")

val actualMethods = rxScalaPublicInstanceAndCompanionMethods.toSet
var good = 0
var bad = 0
for ((javaM, scalaM) <- SortedMap(correspondence.toSeq :_*)) {
if (actualMethods.contains(scalaM) || scalaM.charAt(0) == '[') {
good += 1
} else {
bad += 1
println(s"Warning:")
println(s"$scalaM is NOT present in Scala ${rxScalaType}")
println(s"$javaM is the method in Java ${rxJavaType} generating this warning")
}
}

checkMethodPresenceStatus(good, bad, rxScalaType)
}

def checkMethodPresenceStatus(goodCount: Int, badCount: Int, instance: Any): Unit = {
if (badCount == 0) {
println(s"SUCCESS: $goodCount out of ${badCount+goodCount} methods were found in $instance")
} else {
fail(s"FAILURE: Only $goodCount out of ${badCount+goodCount} methods were found in $instance")
}
}

@Test
def checkJavaMethodPresence(): Unit = {
println("\nTesting that all mentioned Java methods exist")
println("---------------------------------------------\n")
checkMethodPresence(correspondence.keys, rxJavaPublicInstanceAndCompanionMethods, rxJavaType)
}

def checkMethodPresence(expectedMethods: Iterable[String], actualMethods: Iterable[String], tp: Type): Unit = {
val actualMethodsSet = actualMethods.toSet
val expMethodsSorted = expectedMethods.toList.sorted
var good = 0
var bad = 0
for (m <- expMethodsSorted) if (actualMethodsSet.contains(m) || m.charAt(0) == '[') {
good += 1
} else {
bad += 1
println(s"Warning: $m is NOT present in $tp")
}

checkMethodPresenceStatus(good, bad, tp)
}

}
88 changes: 88 additions & 0 deletions src/test/scala/rx/lang/scala/completeness/CompletenessTables.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package rx.lang.scala.completeness

import java.util.Calendar

/**
* Generate comparison tables for Scala classes and Java classes. Run `sbt 'test:run rx.lang.scala.completeness.CompletenessTest'` to generate them.
*/
object CompletenessTables {

/**
* CompletenessKits to generate completeness tables.
*/
val completenessKits = List(
new ObservableCompletenessKit,
new BlockingObservableCompletenessKit,
new TestSchedulerCompletenessKit,
new TestSubscriberCompletenessKit)

def setTodoForMissingMethods(completenessKit: CompletenessKit): Map[String, String] = {
val actualMethods = completenessKit.rxScalaPublicInstanceAndCompanionMethods.toSet
for ((javaM, scalaM) <- completenessKit.correspondence) yield
(javaM, if (actualMethods.contains(scalaM) || scalaM.charAt(0) == '[') scalaM else "[**TODO: missing**]")
}

def scalaToJavaSignature(s: String) =
s.replaceAllLiterally("_ <:", "? extends")
.replaceAllLiterally("_ >:", "? super")
.replaceAllLiterally("[", "<")
.replaceAllLiterally("]", ">")
.replaceAllLiterally("Array<T>", "T[]")

def escapeJava(s: String) =
s.replaceAllLiterally("<", "&lt;")
.replaceAllLiterally(">", "&gt;")


def printMarkdownCorrespondenceTables() {
println("""
---
layout: comparison
title: Comparison of Scala Classes and Java Classes
---

Note:

* These tables contain both static methods and instance methods.
* If a signature is too long, move your mouse over it to get the full signature.
""")

completenessKits.foreach(printMarkdownCorrespondenceTable)

val completenessTablesClassName = getClass.getCanonicalName.dropRight(1) // Drop "$"
println(s"\nThese tables were generated on ${Calendar.getInstance().getTime}.")
println(s"**Do not edit**. Instead, edit `${completenessTablesClassName}` and run `sbt 'test:run ${completenessTablesClassName}'` to generate these tables.")
}

def printMarkdownCorrespondenceTable(completenessKit: CompletenessKit): Unit = {
def groupingKey(p: (String, String)): (String, String) =
(if (p._1.startsWith("average")) "average" else p._1.takeWhile(_ != '('), p._2)
def formatJavaCol(name: String, alternatives: Iterable[String]): String = {
alternatives.toList.sorted.map(scalaToJavaSignature).map(s => {
if (s.length > 64) {
val toolTip = escapeJava(s)
"<span title=\"" + toolTip + "\"><code>" + name + "(...)</code></span>"
} else {
"`" + s + "`"
}
}).mkString("<br/>")
}
def formatScalaCol(s: String): String =
if (s.startsWith("[") && s.endsWith("]")) s.drop(1).dropRight(1) else "`" + s + "`"

val ps = setTodoForMissingMethods(completenessKit)

println(s"""
|## Comparison of Scala ${completenessKit.rxScalaType.typeSymbol.name} and Java ${completenessKit.rxJavaType.typeSymbol.name}
|
|| Java Method | Scala Method |
||-------------|--------------|""".stripMargin)
(for (((javaName, scalaCol), pairs) <- ps.groupBy(groupingKey).toList.sortBy(_._1._1)) yield {
"| " + formatJavaCol(javaName, pairs.map(_._1)) + " | " + formatScalaCol(scalaCol) + " |"
}).foreach(println(_))
}

def main(args: Array[String]): Unit = {
printMarkdownCorrespondenceTables()
}
}
Loading