Skip to content

Commit 2130684

Browse files
restore CompletenessTest
1 parent 32f3fd7 commit 2130684

File tree

1 file changed

+354
-0
lines changed

1 file changed

+354
-0
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
package rx.lang.scala
2+
3+
import java.util.Calendar
4+
5+
import scala.collection.SortedMap
6+
import scala.reflect.runtime.universe
7+
import scala.reflect.runtime.universe.Symbol
8+
import scala.reflect.runtime.universe.Type
9+
import scala.reflect.runtime.universe.typeOf
10+
11+
import org.junit.Ignore
12+
import org.junit.Test
13+
import org.scalatest.junit.JUnitSuite
14+
15+
/**
16+
* These tests can be used to check if all methods of the Java Observable have a corresponding
17+
* method in the Scala Observable.
18+
*
19+
* These tests don't contain any assertions, so they will always succeed, but they print their
20+
* results to stdout.
21+
*/
22+
class CompletenessTest extends JUnitSuite {
23+
24+
// some frequently used comments:
25+
val unnecessary = "[considered unnecessary in Scala land]"
26+
val deprecated = "[deprecated in RxJava]"
27+
val averageProblem = "[We can't have a general average method because Scala's `Numeric` does not have " +
28+
"scalar multiplication (we would need to calculate `(1.0/numberOfElements)*sum`). " +
29+
"You can use `fold` instead to accumulate `sum` and `numberOfElements` and divide at the end.]"
30+
val commentForFirstWithPredicate = "[use `.filter(condition).first`]"
31+
val fromFuture = "[TODO: Decide how Scala Futures should relate to Observables. Should there be a " +
32+
"common base interface for Future and Observable? And should Futures also have an unsubscribe method?]"
33+
34+
/**
35+
* Maps each method from the Java Observable to its corresponding method in the Scala Observable
36+
*/
37+
val correspondence = defaultMethodCorrespondence ++ correspondenceChanges // ++ overrides LHS with RHS
38+
39+
/**
40+
* Creates default method correspondence mappings, assuming that Scala methods have the same
41+
* name and the same argument types as in Java
42+
*/
43+
def defaultMethodCorrespondence: Map[String, String] = {
44+
val allMethods = getPublicInstanceAndCompanionMethods(typeOf[rx.Observable[_]])
45+
val tuples = for (javaM <- allMethods) yield (javaM, javaMethodSignatureToScala(javaM))
46+
tuples.toMap
47+
}
48+
49+
/**
50+
* Manually added mappings from Java Observable methods to Scala Observable methods
51+
*/
52+
def correspondenceChanges = Map(
53+
// manually added entries for Java instance methods
54+
"aggregate(Func2[T, T, T])" -> "reduce((U, U) => U)",
55+
"aggregate(R, Func2[R, _ >: T, R])" -> "foldLeft(R)((R, T) => R)",
56+
"all(Func1[_ >: T, Boolean])" -> "forall(T => Boolean)",
57+
"buffer(Long, Long, TimeUnit)" -> "buffer(Duration, Duration)",
58+
"buffer(Long, Long, TimeUnit, Scheduler)" -> "buffer(Duration, Duration, Scheduler)",
59+
"count()" -> "length",
60+
"dematerialize()" -> "dematerialize(<:<[Observable[T], Observable[Notification[U]]])",
61+
"elementAt(Int)" -> "[use `.drop(index).first`]",
62+
"elementAtOrDefault(Int, T)" -> "[use `.drop(index).firstOrElse(default)`]",
63+
"first(Func1[_ >: T, Boolean])" -> commentForFirstWithPredicate,
64+
"firstOrDefault(T)" -> "firstOrElse(=> U)",
65+
"firstOrDefault(Func1[_ >: T, Boolean], T)" -> "[use `.filter(condition).firstOrElse(default)`]",
66+
"groupBy(Func1[_ >: T, _ <: K], Func1[_ >: T, _ <: R])" -> "[use `groupBy` and `map`]",
67+
"mapMany(Func1[_ >: T, _ <: Observable[_ <: R]])" -> "flatMap(T => Observable[R])",
68+
"mapWithIndex(Func2[_ >: T, Integer, _ <: R])" -> "[combine `zipWithIndex` with `map` or with a for comprehension]",
69+
"onErrorResumeNext(Func1[Throwable, _ <: Observable[_ <: T]])" -> "onErrorResumeNext(Throwable => Observable[U])",
70+
"onErrorResumeNext(Observable[_ <: T])" -> "onErrorResumeNext(Observable[U])",
71+
"onErrorReturn(Func1[Throwable, _ <: T])" -> "onErrorReturn(Throwable => U)",
72+
"onExceptionResumeNext(Observable[_ <: T])" -> "onExceptionResumeNext(Observable[U])",
73+
"parallel(Func1[Observable[T], Observable[R]])" -> "parallel(Observable[T] => Observable[R])",
74+
"parallel(Func1[Observable[T], Observable[R]], Scheduler)" -> "parallel(Observable[T] => Observable[R], Scheduler)",
75+
"reduce(Func2[T, T, T])" -> "reduce((U, U) => U)",
76+
"reduce(R, Func2[R, _ >: T, R])" -> "foldLeft(R)((R, T) => R)",
77+
"scan(Func2[T, T, T])" -> unnecessary,
78+
"scan(R, Func2[R, _ >: T, R])" -> "scan(R)((R, T) => R)",
79+
"skip(Int)" -> "drop(Int)",
80+
"skipWhile(Func1[_ >: T, Boolean])" -> "dropWhile(T => Boolean)",
81+
"skipWhileWithIndex(Func2[_ >: T, Integer, Boolean])" -> unnecessary,
82+
"startWith(Iterable[T])" -> "[unnecessary because we can just use `++` instead]",
83+
"takeFirst()" -> "first",
84+
"takeFirst(Func1[_ >: T, Boolean])" -> commentForFirstWithPredicate,
85+
"takeLast(Int)" -> "takeRight(Int)",
86+
"takeWhileWithIndex(Func2[_ >: T, _ >: Integer, Boolean])" -> "[use `.zipWithIndex.takeWhile{case (elem, index) => condition}.map(_._1)`]",
87+
"toList()" -> "toSeq",
88+
"toSortedList()" -> "[Sorting is already done in Scala's collection library, use `.toSeq.map(_.sorted)`]",
89+
"toSortedList(Func2[_ >: T, _ >: T, Integer])" -> "[Sorting is already done in Scala's collection library, use `.toSeq.map(_.sortWith(f))`]",
90+
"where(Func1[_ >: T, Boolean])" -> "filter(T => Boolean)",
91+
"window(Long, Long, TimeUnit)" -> "window(Duration, Duration)",
92+
"window(Long, Long, TimeUnit, Scheduler)" -> "window(Duration, Duration, Scheduler)",
93+
94+
// manually added entries for Java static methods
95+
"average(Observable[Integer])" -> averageProblem,
96+
"averageDoubles(Observable[Double])" -> averageProblem,
97+
"averageFloats(Observable[Float])" -> averageProblem,
98+
"averageLongs(Observable[Long])" -> averageProblem,
99+
"create(OnSubscribeFunc[T])" -> "apply(Observer[T] => Subscription)",
100+
"combineLatest(Observable[_ <: T1], Observable[_ <: T2], Func2[_ >: T1, _ >: T2, _ <: R])" -> "combineLatest(Observable[U])",
101+
"concat(Observable[_ <: Observable[_ <: T]])" -> "concat(<:<[Observable[T], Observable[Observable[U]]])",
102+
"defer(Func0[_ <: Observable[_ <: T]])" -> "defer(=> Observable[T])",
103+
"empty()" -> "apply(T*)",
104+
"error(Throwable)" -> "apply(Throwable)",
105+
"from(Array[T])" -> "apply(T*)",
106+
"from(Iterable[_ <: T])" -> "apply(T*)",
107+
"from(Future[_ <: T])" -> fromFuture,
108+
"from(Future[_ <: T], Long, TimeUnit)" -> fromFuture,
109+
"from(Future[_ <: T], Scheduler)" -> fromFuture,
110+
"just(T)" -> "apply(T*)",
111+
"merge(Observable[_ <: T], Observable[_ <: T])" -> "merge(Observable[U])",
112+
"merge(Observable[_ <: Observable[_ <: T]])" -> "flatten(<:<[Observable[T], Observable[Observable[U]]])",
113+
"mergeDelayError(Observable[_ <: T], Observable[_ <: T])" -> "mergeDelayError(Observable[U])",
114+
"mergeDelayError(Observable[_ <: Observable[_ <: T]])" -> "flattenDelayError(<:<[Observable[T], Observable[Observable[U]]])",
115+
"range(Int, Int)" -> "apply(Range)",
116+
"sequenceEqual(Observable[_ <: T], Observable[_ <: T])" -> "[use `(first zip second) map (p => p._1 == p._2)`]",
117+
"sequenceEqual(Observable[_ <: T], Observable[_ <: T], Func2[_ >: T, _ >: T, Boolean])" -> "[use `(first zip second) map (p => equality(p._1, p._2))`]",
118+
"sum(Observable[Integer])" -> "sum(Numeric[U])",
119+
"sumDoubles(Observable[Double])" -> "sum(Numeric[U])",
120+
"sumFloats(Observable[Float])" -> "sum(Numeric[U])",
121+
"sumLongs(Observable[Long])" -> "sum(Numeric[U])",
122+
"synchronize(Observable[T])" -> "synchronize",
123+
"switchDo(Observable[_ <: Observable[_ <: T]])" -> deprecated,
124+
"switchOnNext(Observable[_ <: Observable[_ <: T]])" -> "switch(<:<[Observable[T], Observable[Observable[U]]])",
125+
"zip(Observable[_ <: T1], Observable[_ <: T2], Func2[_ >: T1, _ >: T2, _ <: R])" -> "[use instance method `zip` and `map`]",
126+
"zip(Observable[_ <: Observable[_]], FuncN[_ <: R])" -> "[use `zip` in companion object and `map`]",
127+
"zip(Iterable[_ <: Observable[_]], FuncN[_ <: R])" -> "[use `zip` in companion object and `map`]"
128+
) ++ List.iterate("T", 9)(s => s + ", T").map(
129+
// all 9 overloads of startWith:
130+
"startWith(" + _ + ")" -> "[unnecessary because we can just use `++` instead]"
131+
).toMap ++ List.iterate("Observable[_ <: T]", 9)(s => s + ", Observable[_ <: T]").map(
132+
// concat 2-9
133+
"concat(" + _ + ")" -> "[unnecessary because we can use `++` instead or `Observable(o1, o2, ...).concat`]"
134+
).drop(1).toMap ++ List.iterate("T", 10)(s => s + ", T").map(
135+
// all 10 overloads of from:
136+
"from(" + _ + ")" -> "apply(T*)"
137+
).toMap ++ (3 to 9).map(i => {
138+
// zip3-9:
139+
val obsArgs = (1 to i).map(j => s"Observable[_ <: T$j], ").mkString("")
140+
val funcParams = (1 to i).map(j => s"_ >: T$j, ").mkString("")
141+
("zip(" + obsArgs + "Func" + i + "[" + funcParams + "_ <: R])", unnecessary)
142+
}).toMap ++ List.iterate("Observable[_ <: T]", 9)(s => s + ", Observable[_ <: T]").map(
143+
// merge 3-9:
144+
"merge(" + _ + ")" -> "[unnecessary because we can use `Observable(o1, o2, ...).flatten` instead]"
145+
).drop(2).toMap ++ List.iterate("Observable[_ <: T]", 9)(s => s + ", Observable[_ <: T]").map(
146+
// mergeDelayError 3-9:
147+
"mergeDelayError(" + _ + ")" -> "[unnecessary because we can use `Observable(o1, o2, ...).flattenDelayError` instead]"
148+
).drop(2).toMap ++ (3 to 9).map(i => {
149+
// combineLatest 3-9:
150+
val obsArgs = (1 to i).map(j => s"Observable[_ <: T$j], ").mkString("")
151+
val funcParams = (1 to i).map(j => s"_ >: T$j, ").mkString("")
152+
("combineLatest(" + obsArgs + "Func" + i + "[" + funcParams + "_ <: R])", "[If C# doesn't need it, Scala doesn't need it either ;-)]")
153+
}).toMap
154+
155+
def removePackage(s: String) = s.replaceAll("(\\w+\\.)+(\\w+)", "$2")
156+
157+
def methodMembersToMethodStrings(members: Iterable[Symbol]): Iterable[String] = {
158+
for (member <- members; alt <- member.asTerm.alternatives) yield {
159+
val m = alt.asMethod
160+
// multiple parameter lists in case of curried functions
161+
val paramListStrs = for (paramList <- m.paramss) yield {
162+
paramList.map(
163+
symb => removePackage(symb.typeSignature.toString.replaceAll(",(\\S)", ", $1"))
164+
).mkString("(", ", ", ")")
165+
}
166+
val name = alt.asMethod.name.decoded
167+
name + paramListStrs.mkString("")
168+
}
169+
}
170+
171+
def getPublicInstanceMethods(tp: Type): Iterable[String] = {
172+
// declarations: => only those declared in Observable
173+
// members => also those of superclasses
174+
methodMembersToMethodStrings(tp.declarations.filter(m => m.isMethod && m.isPublic))
175+
// TODO how can we filter out instance methods which were put into companion because
176+
// of extends AnyVal in a way which does not depend on implementation-chosen name '$extension'?
177+
.filter(! _.contains("$extension"))
178+
}
179+
180+
// also applicable for Java types
181+
def getPublicInstanceAndCompanionMethods(tp: Type): Iterable[String] =
182+
getPublicInstanceMethods(tp) ++
183+
getPublicInstanceMethods(tp.typeSymbol.companionSymbol.typeSignature)
184+
185+
def printMethodSet(title: String, tp: Type) {
186+
println("\n" + title)
187+
println(title.map(_ => '-') + "\n")
188+
getPublicInstanceMethods(tp).toList.sorted.foreach(println(_))
189+
}
190+
191+
@Ignore // because spams output
192+
@Test def printJavaInstanceMethods: Unit = {
193+
printMethodSet("Instance methods of rx.Observable",
194+
typeOf[rx.Observable[_]])
195+
}
196+
197+
@Ignore // because spams output
198+
@Test def printScalaInstanceMethods: Unit = {
199+
printMethodSet("Instance methods of rx.lang.scala.Observable",
200+
typeOf[rx.lang.scala.Observable[_]])
201+
}
202+
203+
@Ignore // because spams output
204+
@Test def printJavaStaticMethods: Unit = {
205+
printMethodSet("Static methods of rx.Observable",
206+
typeOf[rx.Observable[_]].typeSymbol.companionSymbol.typeSignature)
207+
}
208+
209+
@Ignore // because spams output
210+
@Test def printScalaCompanionMethods: Unit = {
211+
printMethodSet("Companion methods of rx.lang.scala.Observable",
212+
typeOf[rx.lang.scala.Observable.type])
213+
}
214+
215+
def javaMethodSignatureToScala(s: String): String = {
216+
s.replaceAllLiterally("Long, TimeUnit", "Duration")
217+
.replaceAll("Action0", "() => Unit")
218+
// nested [] can't be parsed with regex, so these will have to be added manually
219+
.replaceAll("Action1\\[([^]]*)\\]", "$1 => Unit")
220+
.replaceAll("Action2\\[([^]]*), ([^]]*)\\]", "($1, $2) => Unit")
221+
.replaceAll("Func0\\[([^]]*)\\]", "() => $1")
222+
.replaceAll("Func1\\[([^]]*), ([^]]*)\\]", "$1 => $2")
223+
.replaceAll("Func2\\[([^]]*), ([^]]*), ([^]]*)\\]", "($1, $2) => $3")
224+
.replaceAllLiterally("_ <: ", "")
225+
.replaceAllLiterally("_ >: ", "")
226+
.replaceAll("(\\w+)\\(\\)", "$1")
227+
}
228+
229+
@Ignore // because spams output
230+
@Test def printDefaultMethodCorrespondence: Unit = {
231+
println("\nDefault Method Correspondence")
232+
println( "-----------------------------\n")
233+
val c = SortedMap(defaultMethodCorrespondence.toSeq : _*)
234+
val len = c.keys.map(_.length).max + 2
235+
for ((javaM, scalaM) <- c) {
236+
println(s""" %-${len}s -> %s,""".format("\"" + javaM + "\"", "\"" + scalaM + "\""))
237+
}
238+
}
239+
240+
@Ignore // because spams output
241+
@Test def printCorrectedMethodCorrespondence: Unit = {
242+
println("\nCorrected Method Correspondence")
243+
println( "-------------------------------\n")
244+
val c = SortedMap(correspondence.toSeq : _*)
245+
for ((javaM, scalaM) <- c) {
246+
println("%s -> %s,".format("\"" + javaM + "\"", "\"" + scalaM + "\""))
247+
}
248+
}
249+
250+
def checkMethodPresence(expectedMethods: Iterable[String], tp: Type): Unit = {
251+
val actualMethods = getPublicInstanceAndCompanionMethods(tp).toSet
252+
val expMethodsSorted = expectedMethods.toList.sorted
253+
var good = 0
254+
var bad = 0
255+
for (m <- expMethodsSorted) if (actualMethods.contains(m) || m.charAt(0) == '[') {
256+
good += 1
257+
} else {
258+
bad += 1
259+
println(s"Warning: $m is NOT present in $tp")
260+
}
261+
val status = if (bad == 0) "SUCCESS" else "BAD"
262+
println(s"$status: $bad out of ${bad+good} methods were not found in $tp")
263+
}
264+
265+
@Test def checkScalaMethodPresenceVerbose: Unit = {
266+
println("\nTesting that all mentioned Scala methods exist")
267+
println( "----------------------------------------------\n")
268+
269+
val actualMethods = getPublicInstanceAndCompanionMethods(typeOf[rx.lang.scala.Observable[_]]).toSet
270+
var good = 0
271+
var bad = 0
272+
for ((javaM, scalaM) <- SortedMap(correspondence.toSeq :_*)) {
273+
if (actualMethods.contains(scalaM) || scalaM.charAt(0) == '[') {
274+
good += 1
275+
} else {
276+
bad += 1
277+
println(s"Warning:")
278+
println(s"$scalaM is NOT present in Scala Observable")
279+
println(s"$javaM is the method in Java Observable generating this warning")
280+
}
281+
}
282+
val status = if (bad == 0) "SUCCESS" else "BAD"
283+
println(s"\n$status: $bad out of ${bad+good} methods were not found in Scala Observable")
284+
}
285+
286+
def setTodoForMissingMethods(corresp: Map[String, String]): Map[String, String] = {
287+
val actualMethods = getPublicInstanceAndCompanionMethods(typeOf[rx.lang.scala.Observable[_]]).toSet
288+
for ((javaM, scalaM) <- corresp) yield
289+
(javaM, if (actualMethods.contains(scalaM) || scalaM.charAt(0) == '[') scalaM else "[**TODO: missing**]")
290+
}
291+
292+
@Test def checkJavaMethodPresence: Unit = {
293+
println("\nTesting that all mentioned Java methods exist")
294+
println( "---------------------------------------------\n")
295+
checkMethodPresence(correspondence.keys, typeOf[rx.Observable[_]])
296+
}
297+
298+
@Ignore // because we prefer the verbose version
299+
@Test def checkScalaMethodPresence: Unit = {
300+
checkMethodPresence(correspondence.values, typeOf[rx.lang.scala.Observable[_]])
301+
}
302+
303+
def scalaToJavaSignature(s: String) =
304+
s.replaceAllLiterally("_ <:", "? extends")
305+
.replaceAllLiterally("_ >:", "? super")
306+
.replaceAllLiterally("[", "<")
307+
.replaceAllLiterally("]", ">")
308+
.replaceAllLiterally("Array<T>", "T[]")
309+
310+
def escapeJava(s: String) =
311+
s.replaceAllLiterally("<", "&lt;")
312+
.replaceAllLiterally(">", "&gt;")
313+
314+
@Ignore // because spams output
315+
@Test def printMarkdownCorrespondenceTable() {
316+
def isInteresting(p: (String, String)): Boolean =
317+
p._1.replaceAllLiterally("()", "") != p._2
318+
def groupingKey(p: (String, String)): (String, String) =
319+
(if (p._1.startsWith("average")) "average" else p._1.takeWhile(_ != '('), p._2)
320+
def formatJavaCol(name: String, alternatives: Iterable[String]): String = {
321+
alternatives.toList.sorted.map(scalaToJavaSignature(_)).map(s => {
322+
if (s.length > 64) {
323+
val toolTip = escapeJava(s)
324+
"<span title=\"" + toolTip + "\"><code>" + name + "(...)</code></span>"
325+
} else {
326+
"`" + s + "`"
327+
}
328+
}).mkString("<br/>")
329+
}
330+
def formatScalaCol(s: String): String =
331+
if (s.startsWith("[") && s.endsWith("]")) s.drop(1).dropRight(1) else "`" + s + "`"
332+
def escape(s: String) = s.replaceAllLiterally("[", "&lt;").replaceAllLiterally("]", "&gt;")
333+
334+
println("""
335+
## Comparison of Scala Observable and Java Observable
336+
337+
Note:
338+
* This table contains both static methods and instance methods.
339+
* If a signature is too long, move your mouse over it to get the full signature.
340+
341+
342+
| Java Method | Scala Method |
343+
|-------------|--------------|""")
344+
345+
val ps = setTodoForMissingMethods(correspondence)
346+
347+
(for (((javaName, scalaCol), pairs) <- ps.groupBy(groupingKey(_)).toList.sortBy(_._1._1)) yield {
348+
"| " + formatJavaCol(javaName, pairs.map(_._1)) + " | " + formatScalaCol(scalaCol) + " |"
349+
}).foreach(println(_))
350+
println(s"\nThis table was generated on ${Calendar.getInstance().getTime()}.")
351+
println(s"**Do not edit**. Instead, edit `${getClass().getCanonicalName()}`.")
352+
}
353+
354+
}

0 commit comments

Comments
 (0)