-
Notifications
You must be signed in to change notification settings - Fork 110
Refactor CompletenessTest to support adding completeness tests for other classes #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
50e8ac8
Refactor CompletenessTest to support adding completeness tests for ot…
zsxwing 3b8aeb3
Disable parallel execution for unit tests
zsxwing bdae1eb
Fix BlockingObservable.mostRecent type parameter
zsxwing 46030e3
Add isOmittingParenthesesForArity0Method
zsxwing 8fef6fb
Add utility methods back for maintenance
zsxwing bdd0d30
Revert "Fix BlockingObservable.mostRecent type parameter"
samuelgruetter f63d5b5
add test which illustrates why BlockingObservable.mostRecent needs an…
samuelgruetter cf188b1
Merge pull request #4 from samuelgruetter/completeness-kit-2
zsxwing File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
40 changes: 40 additions & 0 deletions
40
src/test/scala/rx/lang/scala/completeness/BlockingObservableCompletenessKit.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could also change the signature of RxScala's |
||
| "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
181
src/test/scala/rx/lang/scala/completeness/CompletenessKit.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
88
src/test/scala/rx/lang/scala/completeness/CompletenessTables.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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("<", "<") | ||
| .replaceAllLiterally(">", ">") | ||
|
|
||
|
|
||
| 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() | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.,
I guess some reflection APIs are not thread-safe (or a bug?). So if running multiple
CompletenessKitat the same time, it will crash.However, since RxScala's unit tests are pretty fast, disabling it hurts nothing.