import org.jetbrains.compose.gradle.standardConf plugins { kotlin("multiplatform") id("org.jetbrains.compose") } kotlin { js(IR) { browser() { testTask { useKarma { standardConf() } } } binaries.executable() } sourceSets { val jsMain by getting { dependencies { implementation(project(":compose-compiler-integration-lib")) implementation(kotlin("stdlib-js")) implementation(compose.runtime) implementation(project(":html-core")) } } val jsTest by getting { dependencies { implementation(kotlin("test-js")) } } } } fun cloneTemplate(templateName: String, contentMain: String, contentLib: String): File { val tempDir = file("${project.buildDir.absolutePath}/temp/cloned-$templateName") tempDir.deleteRecursively() tempDir.mkdirs() file("${projectDir.absolutePath}/main-template").copyRecursively(tempDir) // tempDir.deleteOnExit() File("$tempDir/src/commonMain/kotlin/Main.kt").printWriter().use { out -> out.println(contentMain) } File("$tempDir/lib/src/commonMain/kotlin/Lib.kt").printWriter().use { out -> out.println(contentLib) } return tempDir } fun build( caseName: String, directory: File, failureExpected: Boolean = false, composeVersion: String, kotlinVersion: String, vararg buildCmd: String = arrayOf("build", "jsNodeRun") ) { val isWin = System.getProperty("os.name").startsWith("Win") val arguments = buildCmd.toMutableList().also { it.add("-Pcompose.version=$composeVersion") it.add("-Pkotlin.version=$kotlinVersion") }.toTypedArray() val procBuilder = if (isWin) { ProcessBuilder("gradlew.bat", *arguments) } else { ProcessBuilder("bash", "./gradlew", *arguments) } val proc = procBuilder .directory(directory) .redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE) .start() proc.waitFor(5, TimeUnit.MINUTES) "(COMPOSE_INTEGRATION_VERSION=\\[.*\\])".toRegex().find( proc.inputStream.bufferedReader().readText() )?.also { println(it.groupValues[1]) } println(proc.errorStream.bufferedReader().readText()) if (proc.exitValue() != 0 && !failureExpected) { throw GradleException("Error compiling $caseName") } if (failureExpected && proc.exitValue() == 0) { throw AssertionError("$caseName compilation did not fail!!!") } } data class RunChecksResult( val cases: Map ) { val totalCount = cases.size val failedCount = cases.filter { it.value != null }.size val hasFailed = failedCount > 0 fun printResults() { cases.forEach { (name, throwable) -> println(name + " : " + (throwable ?: "OK")) } } fun reportToTeamCity() { cases.forEach { (caseName, error) -> println("##teamcity[testStarted name='compileTestCase_$caseName']") if (error != null) { println("##teamcity[testFailed name='compileTestCase_$caseName']") } println("##teamcity[testFinished name='compileTestCase_$caseName']") } } } fun runCasesInDirectory( dir: File, filterPath: String, expectCompilationError: Boolean, composeVersion: String, kotlinVersion: String ): RunChecksResult { return dir.listFiles()!!.filter { it.absolutePath.contains(filterPath) }.mapIndexed { _, file -> println("Running check for ${file.name}, expectCompilationError = $expectCompilationError, composeVersion = $composeVersion") val contentLines = file.readLines() val startMainLineIx = contentLines.indexOf("// @Module:Main").let { ix -> if (ix == -1) 0 else ix + 1 } val startLibLineIx = contentLines.indexOf("// @Module:Lib").let { ix -> if (ix == -1) contentLines.size else ix - 1 } require(startMainLineIx < startLibLineIx) { "The convention is that @Module:Lib should go after @Module:Main" } val mainContent = contentLines.let { lines -> val endLineIx = if (startLibLineIx < lines.size) startLibLineIx - 1 else lines.lastIndex lines.slice(startMainLineIx..endLineIx).joinToString(separator = "\n") } val libContent = contentLines.let { lines -> if (startLibLineIx < lines.size) { lines.slice(startLibLineIx..lines.lastIndex) } else { emptyList() }.joinToString(separator = "\n") } val caseName = file.name val tmpDir = cloneTemplate(caseName, contentMain = mainContent, contentLib = libContent) caseName to kotlin.runCatching { build( caseName = caseName, directory = tmpDir, failureExpected = expectCompilationError, composeVersion = composeVersion, kotlinVersion = kotlinVersion ) }.exceptionOrNull() }.let { RunChecksResult(it.toMap()) } } tasks.register("checkComposeCases") { doLast { val filterCases = project.findProperty("FILTER_CASES")?.toString() ?: "" val composeVersion = project.findProperty("compose.version")?.toString() ?: "0.0.0-SNASPHOT" val kotlinVersion = kotlin.coreLibrariesVersion val expectedFailingCasesDir = File("${projectDir.absolutePath}/testcases/failing") val expectedFailingResult = runCasesInDirectory( dir = expectedFailingCasesDir, expectCompilationError = true, filterPath = filterCases, composeVersion = composeVersion, kotlinVersion = kotlinVersion ) val passingCasesDir = File("${projectDir.absolutePath}/testcases/passing") val passingResult = runCasesInDirectory( dir = passingCasesDir, expectCompilationError = false, filterPath = filterCases, composeVersion = composeVersion, kotlinVersion = kotlinVersion ) expectedFailingResult.printResults() expectedFailingResult.reportToTeamCity() passingResult.printResults() passingResult.reportToTeamCity() if (expectedFailingResult.hasFailed || passingResult.hasFailed) { error("There were failed cases. Check the logs above") } } }