Improve usability of PublicApiTest:

* No need to manually create tests for new modules (scans list of modules)
* Pass test when used with "-Poverwrite.output=true"
diff --git a/binary-compatibility-validator/resources/api.properties b/binary-compatibility-validator/resources/api.properties
new file mode 100644
index 0000000..16f2f78
--- /dev/null
+++ b/binary-compatibility-validator/resources/api.properties
@@ -0,0 +1,9 @@
+#
+# Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+module.roots=core integration native reactive ui
+module.marker=build.gradle
+module.ignore=kotlinx-coroutines-rx-example
+
+packages.internal=kotlinx.coroutines.experimental.internal kotlinx.coroutines.experimental.scheduling
\ No newline at end of file
diff --git a/binary-compatibility-validator/test/PublicApiTest.kt b/binary-compatibility-validator/test/PublicApiTest.kt
index 5fe5c1e..0384bf7 100644
--- a/binary-compatibility-validator/test/PublicApiTest.kt
+++ b/binary-compatibility-validator/test/PublicApiTest.kt
@@ -5,124 +5,62 @@
 package kotlinx.coroutines.experimental.tools
 
 import org.junit.*
-import org.junit.rules.*
+import org.junit.runner.*
+import org.junit.runners.*
 import java.io.*
+import java.util.*
 import java.util.jar.*
+import kotlin.collections.ArrayList
 
-class PublicApiTest {
+@RunWith(Parameterized::class)
+class PublicApiTest(
+    private val rootDir: String,
+    private val moduleName: String
+) {
+    companion object {
+        private val apiProps = ClassLoader.getSystemClassLoader()
+            .getResource("api.properties").openStream().use { Properties().apply { load(it) } }
+        private val nonPublicPackages = apiProps.getProperty("packages.internal")!!.split(" ")
 
-   /*
-    * How to add a test for your module kotlinx-coroutines-foo?
-    *
-    * Dump public declarations via PublicApiDump.kt and create file
-    * reference-public-api/kotlinx-coroutines-foo.txt with dumped declarations.
-    *
-    * Then add test:
-    *
-    * @Test
-    * fun kotlinxCorountesFoo() { // <- name pattern should match txt file from reference-public-api
-    *   snapshotAPIAndCompare($relative_path_to_module)
-    * }
-    */
-
-    @Rule
-    @JvmField
-    val testName = TestName()
-
-    @Test
-    fun kotlinxCoroutinesCore() {
-        snapshotAPIAndCompare("core/kotlinx-coroutines-core", nonPublicPackages = listOf(
-            "kotlinx.coroutines.experimental.internal",
-            "kotlinx.coroutines.experimental.scheduling"))
+        @Parameterized.Parameters(name = "{1}")
+        @JvmStatic
+        fun modules(): List<Array<Any>> {
+            val moduleRoots = apiProps.getProperty("module.roots").split(" ")
+            val moduleMarker = apiProps.getProperty("module.marker")!!
+            val moduleIgnore = apiProps.getProperty("module.ignore")!!.split(" ").toSet()
+            val modules = ArrayList<Array<Any>>()
+            for (rootDir in moduleRoots) {
+                File("../$rootDir").listFiles( FileFilter { it.isDirectory })?.forEach { dir ->
+                    if (dir.name !in moduleIgnore && File(dir, moduleMarker).exists()) {
+                        modules += arrayOf<Any>(rootDir, dir.name)
+                    }
+                }
+            }
+            return modules
+        }
     }
 
     @Test
-    fun kotlinxCoroutinesReactive() {
-        snapshotAPIAndCompare("reactive/kotlinx-coroutines-reactive")
-    }
-
-    @Test
-    fun kotlinxCoroutinesReactor() {
-        snapshotAPIAndCompare("reactive/kotlinx-coroutines-reactor")
-    }
-
-    @Test
-    fun kotlinxCoroutinesRx1() {
-        snapshotAPIAndCompare("reactive/kotlinx-coroutines-rx1")
-    }
-
-    @Test
-    fun kotlinxCoroutinesRx2() {
-        snapshotAPIAndCompare("reactive/kotlinx-coroutines-rx2")
-    }
-
-    @Test
-    fun kotlinxCoroutinesGuava() {
-        snapshotAPIAndCompare("integration/kotlinx-coroutines-guava")
-    }
-
-    @Test
-    fun kotlinxCoroutinesJdk8() {
-        snapshotAPIAndCompare("integration/kotlinx-coroutines-jdk8")
-    }
-
-
-    @Test
-    fun kotlinxCoroutinesNio() {
-        snapshotAPIAndCompare("integration/kotlinx-coroutines-nio")
-    }
-
-    @Test
-    fun kotlinxCoroutinesQuasar() {
-        snapshotAPIAndCompare("integration/kotlinx-coroutines-quasar")
-    }
-
-    @Test
-    fun kotlinxCoroutinesAndroid() {
-        snapshotAPIAndCompare("ui/kotlinx-coroutines-android")
-    }
-
-
-    @Test
-    fun kotlinxCoroutinesJavafx() {
-        snapshotAPIAndCompare("ui/kotlinx-coroutines-javafx")
-    }
-
-    @Test
-    fun kotlinxCoroutinesSwing() {
-        snapshotAPIAndCompare("ui/kotlinx-coroutines-swing")
-    }
-
-    private fun snapshotAPIAndCompare(basePath: String, jarPattern: String = basePath.substring(basePath.indexOf("/") + 1),
-                                      publicPackages: List<String> = emptyList(), nonPublicPackages: List<String> = emptyList()) {
-        val base = File("../$basePath/build/libs").absoluteFile.normalize()
-        val jarFile = getJarPath(base, jarPattern)
-        val kotlinJvmMappingsFiles = listOf(base.resolve("../visibilities.json"))
-
-        val publicPackagePrefixes = publicPackages.map { it.replace('.', '/') + '/' }
+    fun testApi() {
+        val libsDir = File("../$rootDir/$moduleName/build/libs").absoluteFile.normalize()
+        val jarFile = getJarPath(libsDir)
+        val kotlinJvmMappingsFiles = listOf(libsDir.resolve("../visibilities.json"))
         val visibilities =
                 kotlinJvmMappingsFiles
-                        .map { readKotlinVisibilities(it).filterKeys { name -> publicPackagePrefixes.none { name.startsWith(it) } } }
+                        .map { readKotlinVisibilities(it) }
                         .reduce { m1, m2 -> m1 + m2 }
-
         val api = getBinaryAPI(JarFile(jarFile), visibilities).filterOutNonPublic(nonPublicPackages)
-
-        val target = File("reference-public-api")
-                .resolve(testName.methodName.replaceCamelCaseWithDashedLowerCase() + ".txt")
-
-        api.dumpAndCompareWith(target)
+        api.dumpAndCompareWith(File("reference-public-api").resolve("$moduleName.txt"))
     }
 
-    private fun getJarPath(base: File, jarPattern: String, kotlinVersion: String? = null): File {
-        val versionPattern = kotlinVersion?.let { "-" + Regex.escape(it) } ?: ".+"
-        val regex = Regex("$jarPattern$versionPattern\\.jar")
-        val files = (base.listFiles() ?: throw Exception("Cannot list files in $base"))
+    private fun getJarPath(libsDir: File): File {
+        val regex = Regex("$moduleName-.+\\.jar")
+        val files = (libsDir.listFiles() ?: throw Exception("Cannot list files in $libsDir"))
             .filter { it.name.let {
                     it matches regex
                     && !it.endsWith("-sources.jar")
                     && !it.endsWith("-javadoc.jar")
                     && !it.endsWith("-tests.jar")} }
-
-        return files.singleOrNull() ?: throw Exception("No single file matching $regex in $base:\n${files.joinToString("\n")}")
+        return files.singleOrNull() ?: throw Exception("No single file matching $regex in $libsDir:\n${files.joinToString("\n")}")
     }
 }
diff --git a/binary-compatibility-validator/test/utils.kt b/binary-compatibility-validator/test/utils.kt
index e499407..89b844f 100644
--- a/binary-compatibility-validator/test/utils.kt
+++ b/binary-compatibility-validator/test/utils.kt
@@ -14,26 +14,28 @@
     if (!to.exists()) {
         to.parentFile?.mkdirs()
         to.bufferedWriter().use { dump(to = it) }
-        fail("Expected data file did not exist. Generating: $to")
+        fail("Expected data file did not exist. Generated: $to")
     } else {
         val actual = dump(to = StringBuilder())
         assertEqualsToFile(to, actual)
     }
 }
 
-private fun assertEqualsToFile(expectedFile: File, actual: CharSequence) {
+private fun assertEqualsToFile(to: File, actual: CharSequence) {
     val actualText = actual.trimTrailingWhitespacesAndAddNewlineAtEOF()
-    val expectedText = expectedFile.readText().trimTrailingWhitespacesAndAddNewlineAtEOF()
-
-    if (OVERWRITE_EXPECTED_OUTPUT && expectedText != actualText) {
-        expectedFile.writeText(actualText)
-        assertEquals(expectedText, actualText, "Actual data differs from file content: ${expectedFile.name}, rewriting")
+    val expectedText = to.readText().trimTrailingWhitespacesAndAddNewlineAtEOF()
+    if (expectedText == actualText) return // Ok
+    // Difference
+    if (OVERWRITE_EXPECTED_OUTPUT) {
+        to.writeText(actualText)
+        println("Generated: $to")
+        return // make test pass when overwriting output
     }
-
+    // Fail on difference
     assertEquals(
         expectedText,
         actualText,
-        "Actual data differs from file content: ${expectedFile.name}\nTo overwrite the expected API rerun with -Doverwrite.output=true parameter\n"
+        "Actual data differs from file content: ${to.name}\nTo overwrite the expected API rerun with -Doverwrite.output=true parameter\n"
     )
 }
 
@@ -44,4 +46,3 @@
 
 
 private val UPPER_CASE_CHARS = Regex("[A-Z]+")
-fun String.replaceCamelCaseWithDashedLowerCase() = replace(UPPER_CASE_CHARS) { "-" + it.value.toLowerCase() }