Split android box tests by flavours

This commit is contained in:
Mikhael Bogdanov
2019-01-29 16:02:59 +01:00
parent 7c77565bb8
commit 0774f800a0
12 changed files with 195 additions and 170 deletions
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<option name="DEFAULT_COMPILER" value="Javac" />
<excludeFromCompile>
<directory url="file://$PROJECT_DIR$/gen" includeSubdirectories="true" />
</excludeFromCompile>
</component>
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/android-module.iml" filepath="$PROJECT_DIR$/android_module.iml" />
</modules>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true" project-jdk-name="Android_SDK" project-jdk-type="Android SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
@@ -5,9 +5,8 @@ buildscript {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@@ -31,7 +30,6 @@ android {
versionName "1.0"
testApplicationId "org.jetbrains.kotlin.android.tests.gradle"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
buildTypes {
debug {
@@ -62,6 +60,19 @@ android {
resultsDir = "build/test/results"
}
flavorDimensions "box"
productFlavors {
ktest0 {
dimension "box"
}
ktest1 {
dimension "box"
}
}
}
task jarTestFolders() {
@@ -81,7 +92,8 @@ tasks.withType(JavaCompile) {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
androidTestImplementation 'junit:junit:4.12'
implementation 'com.android.support:multidex:1.0.3'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation fileTree(dir: 'libs/test', include: ['*.jar'])
ktest0Implementation fileTree(dir: 'libs/test', include: ['libtest0.jar'])
ktest1Implementation fileTree(dir: 'libs/test', include: ['libtest1.jar'])
}
@@ -1,14 +0,0 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}\tools\proguard\proguard-android.txt:proguard-project.txt
# Project target.
target=android-16
+4
View File
@@ -53,3 +53,7 @@ projectTest {
workingDir = rootDir
}
val generateTests by generator("org.jetbrains.kotlin.android.tests.CodegenTestsOnAndroidGenerator")
generateTests.workingDir = rootDir
@@ -30,6 +30,7 @@ import java.io.File
import java.io.IOException
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import kotlin.test.assertTrue
class CodegenTestsOnAndroidRunner private constructor(private val pathManager: PathManager) {
@@ -59,7 +60,7 @@ class CodegenTestsOnAndroidRunner private constructor(private val pathManager: P
rootSuite.addTest(this)
}
renameReport()
renameFlavorFolder()
enableD8(true)
runTestsOnEmulator(gradleRunner, TestSuite("D8")).apply {
(0 until this.countTestCases()).forEach {
@@ -95,22 +96,27 @@ class CodegenTestsOnAndroidRunner private constructor(private val pathManager: P
}
private fun processReport(suite: TestSuite, resultOutput: String) {
val reportFolder = reportFolder()
val reportFolder = File(flavorFolder())
try {
val testCases = parseSingleReportInFolder(reportFolder)
testCases.forEach { aCase -> suite.addTest(aCase) }
Assert.assertNotEquals("There is no test results in report", 0, testCases.size.toLong())
val folders = reportFolder.listFiles()
assertTrue(folders != null && folders.isNotEmpty(), "No folders in ${reportFolder.path}")
folders.forEach {
assertTrue("${it.path} is not directory") { it.isDirectory }
val testCases = parseSingleReportInFolder(it)
testCases.forEach { aCase -> suite.addTest(aCase) }
Assert.assertNotEquals("There is no test results in report", 0, testCases.size.toLong())
}
} catch (e: Exception) {
throw RuntimeException("Can't parse test results in $reportFolder\n$resultOutput", e)
}
}
private fun renameReport() {
val reportFolder = File(reportFolder())
private fun renameFlavorFolder() {
val reportFolder = File(flavorFolder())
reportFolder.renameTo(File(reportFolder.parentFile, reportFolder.name + "_dex"))
}
private fun reportFolder() = pathManager.tmpFolder + "/build/test/results/connected/"
private fun flavorFolder() = pathManager.tmpFolder + "/build/test/results/connected/flavors"
private fun runTestsOnEmulator(gradleRunner: GradleRunner, suite: TestSuite): TestSuite {
val platformPrefixProperty = System.setProperty(PlatformUtils.PLATFORM_PREFIX_KEY, "Idea")
@@ -153,11 +159,10 @@ class CodegenTestsOnAndroidRunner private constructor(private val pathManager: P
}
@Throws(IOException::class, SAXException::class, ParserConfigurationException::class)
private fun parseSingleReportInFolder(reportFolder: String): List<TestCase> {
val folder = File(reportFolder)
private fun parseSingleReportInFolder(folder: File): List<TestCase> {
val files = folder.listFiles()!!
assert(files.size == 1) {
"Expecting one file but ${files.size}: ${files.joinToString { it.name }}"
"Expecting one file but ${files.size}: ${files.joinToString { it.name }} in ${folder.path}"
}
val reportFile = files[0]
@@ -60,10 +60,6 @@ public class PathManager {
return tmpFolder + "/libs/libtest" + index;
}
public String getLibsFolderInAndroidTestedModuleTmpFolder() {
return tmpFolder + "/tested-module/libs";
}
public String getLibsFolderInAndroidTmpFolder() {
return tmpFolder + "/libs";
}
@@ -43,7 +43,7 @@ public class SDKDownloader {
private static final String SDK_TOOLS = "4333796"; //"26.1.1";
public static final String BUILD_TOOLS = "28.0.3";
private static final int ANDROID_VERSION = 19;
public static final String GRADLE_VERSION = "4.6";
public static final String GRADLE_VERSION = "5.1.1";
public static final String EMULATOR_TOOLS_VERSION = "5264690"; //"28.0.23";
@@ -70,8 +70,7 @@ public class GradleRunner {
System.out.println("Starting tests...");
GeneralCommandLine test = generateCommandLine("connectedAndroidTest");
test.addParameters("--stacktrace");
//To avoid problem with discovering tests on Android in multidex build
test.addParameters("-Pandroid.testInstrumentationRunnerArguments.class=org.jetbrains.kotlin.android.tests.CodegenTestCaseOnAndroid");
test.addParameters("--continue"); //run all flavors even if any fail
return RunUtils.execute(test).getOutput();
}
@@ -19,14 +19,11 @@ package org.jetbrains.kotlin.android.tests;
import com.google.common.io.Files;
import com.intellij.openapi.util.io.FileUtil;
import junit.framework.TestSuite;
import kotlin.io.FilesKt;
import org.jetbrains.annotations.NotNull;
import org.junit.runner.RunWith;
import org.junit.runners.AllTests;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
@RunWith(AllTests.class)
public class AndroidRunner {
@@ -47,9 +44,6 @@ public class AndroidRunner {
public static TestSuite suite() throws Throwable {
PathManager pathManager = getPathManager();
FileUtil.copyDir(new File(pathManager.getAndroidModuleRoot()), new File(pathManager.getTmpFolder()));
writeAndroidSkdToLocalProperties();
CodegenTestsOnAndroidGenerator.generate(pathManager);
System.out.println("Run tests on android...");
@@ -65,12 +59,4 @@ public class AndroidRunner {
FileUtil.delete(new File(pathManager.getTmpFolder()));
}
private static void writeAndroidSkdToLocalProperties() throws IOException {
String sdkRoot = FilesKt.getInvariantSeparatorsPath(new File(pathManager.getAndroidSdkRoot()));
System.out.println("Writing android sdk to local.properties: " + sdkRoot);
File file = new File(pathManager.getTmpFolder() + "/local.properties");
try (FileWriter fw = new FileWriter(file)) {
fw.write("sdk.dir=" + sdkRoot);
}
}
}
@@ -32,7 +32,7 @@ private val importPattern = Pattern.compile("import[ |\t]([\\w|]*\\.)")
private data class OldPackageAndNew(val oldFqName: FqName, val newFqName: FqName)
internal fun patchFiles(
internal fun patchFilesAndAddTest(
testFile: File,
testFiles: List<CodegenTestCase.TestFile>,
filesHolder: CodegenTestsOnAndroidGenerator.FilesWriter
@@ -103,16 +103,16 @@ internal fun patchFiles(
file.content = file.content.patchSelfImports(file.newPackage)
}
resultFiles.forEach { resultFile ->
if (resultFile.name.endsWith(".kt") || resultFile.name.endsWith(".kts")) {
filesHolder.addFile(resultFile.name, resultFile.content)
}
}
val boxFiles = resultFiles.filter { hasBoxMethod(it.content) }
if (boxFiles.size != 1) {
println("Several box methods in $testFile")
}
filesHolder.addTest(
resultFiles.filter { resultFile -> resultFile.name.endsWith(".kt") || resultFile.name.endsWith(".kts") },
TestInfo("", boxFiles.last().newClassId, testFile)
)
return boxFiles.last().newClassId
}
@@ -5,11 +5,9 @@
package org.jetbrains.kotlin.android.tests
import com.google.common.collect.Lists
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.cli.common.output.writeAllTo
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
@@ -20,16 +18,13 @@ import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.*
import org.jetbrains.kotlin.utils.Printer
import org.junit.Assert
import org.junit.Ignore
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.util.*
data class ConfigurationKey(val kind: ConfigurationKind, val jdkKind: TestJdkKind, val configuration: String)
@@ -39,24 +34,31 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
private var currentModuleIndex = 1
private val generatedTestNames = Lists.newArrayList<String>()
private val pathFilter: String? = System.getProperties().getProperty("kotlin.test.android.path.filter")
private fun generateOutputFiles() {
private val pendingUnitTestGenerators = hashMapOf<Int, UnitTestFileWriter>()
//keep it globally to avoid test grouping on TC
private val generatedTestNames = hashSetOf<String>()
fun getFlavorUnitTestFilePath(index: Int): String {
return pathManager.srcFolderInAndroidTmpFolder + "/androidTestKtest$index/java/" + testClassPackage.replace(
".",
"/"
) + "/" + testClassName + "$index.java"
}
private fun prepareAndroidModuleAndGenerateTests() {
prepareAndroidModule()
generateAndSave()
generateTestsAndFlavourSuites()
}
private fun prepareAndroidModule() {
FileUtil.copyDir(File(pathManager.androidModuleRoot), File(pathManager.tmpFolder))
writeAndroidSkdToLocalProperties(pathManager)
println("Copying kotlin-stdlib.jar and kotlin-reflect.jar in android module...")
copyKotlinRuntimeJars()
println("Check 'libs' folder in tested android module...")
val libsFolderInTestedModule = File(pathManager.libsFolderInAndroidTestedModuleTmpFolder)
if (!libsFolderInTestedModule.exists()) {
libsFolderInTestedModule.mkdirs()
}
}
private fun copyKotlinRuntimeJars() {
@@ -75,38 +77,20 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
)
}
private fun generateAndSave() {
private fun generateTestsAndFlavourSuites() {
println("Generating test files...")
val testSourceFilePath =
pathManager.srcFolderInAndroidTmpFolder + "/androidTest/java/" + testClassPackage.replace(".", "/") + "/" + testClassName + ".java"
FileWriter(File(testSourceFilePath).also { it.parentFile.mkdirs() }).use {
val p = Printer(it)
p.println(
"""package $testClassPackage;
|
|import $baseTestClassPackage.$baseTestClassName;
|
|/* This class is generated by $generatorName. DO NOT MODIFY MANUALLY */
|public class $testClassName extends $baseTestClassName {
|
""".trimMargin()
)
p.pushIndent()
generateTestMethodsForDirectories(File("compiler/testData/codegen/box"), File("compiler/testData/codegen/boxInline"))
generateTestMethodsForDirectories(p, File("compiler/testData/codegen/box"), File("compiler/testData/codegen/boxInline"))
p.popIndent()
p.println("}")
}
pendingUnitTestGenerators.values.forEach { it.generate() }
}
private fun generateTestMethodsForDirectories(p: Printer, vararg dirs: File) {
private fun generateTestMethodsForDirectories(vararg dirs: File) {
val holders = mutableMapOf<ConfigurationKey, FilesWriter>()
for (dir in dirs) {
val files = dir.listFiles() ?: error("Folder with testData is empty: ${dir.absolutePath}")
processFiles(p, files, holders)
processFiles(files, holders)
}
holders.values.forEach {
@@ -117,7 +101,8 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
internal inner class FilesWriter(
private val configuration: CompilerConfiguration
) {
private val rawFiles: MutableList<Pair<String, String>> = ArrayList()
private val rawFiles = arrayListOf<TestClassInfo>()
private val unitTestDescriptions = arrayListOf<TestInfo>()
private fun shouldWriteFilesOnDisk(): Boolean = rawFiles.size > 300
@@ -139,27 +124,26 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
writeFiles(
rawFiles.map {
try {
CodegenTestFiles.create(it.first, it.second, environment.project).psiFile
CodegenTestFiles.create(it.name, it.content, environment.project).psiFile
} catch (e: Throwable) {
throw RuntimeException("Error on processing ${it.first}:\n${it.second}", e)
throw RuntimeException("Error on processing ${it.name}:\n${it.content}", e)
}
}, environment
)
Disposer.dispose(disposable)
rawFiles.clear()
}
fun addFile(name: String, content: String) {
rawFiles.add(name to content)
unitTestDescriptions.clear()
}
private fun writeFiles(filesToCompile: List<KtFile>, environment: KotlinCoreEnvironment) {
if (filesToCompile.isEmpty()) return
//1000 files per folder, each folder would be jared by build.gradle script
// We can't create one big jar with all test cause dex has problem with memory on teamcity
//3000 files per folder that would be used by flavor to avoid multidex usage,
// each folder would be jared by build.gradle script
writtenFilesCount += filesToCompile.size
val outputDir = File(pathManager.getOutputForCompiledFiles(writtenFilesCount / 1000))
val index = writtenFilesCount / 3000
val outputDir = File(pathManager.getOutputForCompiledFiles(index))
assertTrue("Add flavors for ktest$index", index < 2)
println("Generating ${filesToCompile.size} files into ${outputDir.name}, configuration: '${environment.configuration}'...")
@@ -169,14 +153,25 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
outputDir.mkdirs()
}
Assert.assertTrue("Cannot create directory for compiled files", outputDir.exists())
val unitTestFileWriter = pendingUnitTestGenerators.getOrPut(index) {
UnitTestFileWriter(
getFlavorUnitTestFilePath(index),
index,
generatedTestNames
)
}
unitTestFileWriter.addTests(unitTestDescriptions)
outputFiles.writeAllTo(outputDir)
}
fun addTest(testFiles: List<TestClassInfo>, info: TestInfo) {
rawFiles.addAll(testFiles)
unitTestDescriptions.add(info)
}
}
@Throws(IOException::class)
private fun processFiles(
printer: Printer,
files: Array<File>,
holders: MutableMap<ConfigurationKey, FilesWriter>
) {
@@ -191,7 +186,7 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
if (file.isDirectory) {
val listFiles = file.listFiles()
if (listFiles != null) {
processFiles(printer, listFiles, holders)
processFiles(listFiles, holders)
}
} else if (FileUtilRt.getExtension(file.name) != KotlinFileType.EXTENSION) {
// skip non kotlin files
@@ -204,9 +199,8 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
continue
}
val fullFileText = FileUtil.loadFile(file, true).let {
it.replace("COROUTINES_PACKAGE", "kotlin.coroutines")
}
val fullFileText =
FileUtil.loadFile(file, true).replace("COROUTINES_PACKAGE", "kotlin.coroutines")
if (fullFileText.contains("// WITH_COROUTINES")) {
if (fullFileText.contains("kotlin.coroutines.experimental")) continue
@@ -237,70 +231,57 @@ class CodegenTestsOnAndroidGenerator private constructor(private val pathManager
})
}
val classWithBoxMethod = patchFiles(file, testFiles, filesHolder) ?: continue
val generatedTestName = generateTestName(file.name)
generateTestMethod(
printer,
generatedTestName,
classWithBoxMethod.asString(),
StringUtil.escapeStringCharacters(file.path)
)
patchFilesAndAddTest(file, testFiles, filesHolder)
}
}
}
}
private fun createTestFiles(file: File, expectedText: String): List<CodegenTestCase.TestFile> =
private fun createTestFiles(file: File, expectedText: String): List<TestFile> =
KotlinTestUtils.createTestFiles(
file.name,
expectedText,
object : KotlinTestUtils.TestFileFactoryNoModules<CodegenTestCase.TestFile>() {
override fun create(fileName: String, text: String, directives: Map<String, String>): CodegenTestCase.TestFile {
return CodegenTestCase.TestFile(fileName, text)
object : KotlinTestUtils.TestFileFactoryNoModules<TestFile>() {
override fun create(fileName: String, text: String, directives: Map<String, String>): TestFile {
return TestFile(fileName, text)
}
}, false,
"kotlin.coroutines"
)
private fun generateTestName(fileName: String): String {
var result = NameUtils.sanitizeAsJavaIdentifier(FileUtil.getNameWithoutExtension(StringUtil.capitalize(fileName)))
var i = 0
while (generatedTestNames.contains(result)) {
result += "_" + i++
}
generatedTestNames.add(result)
return result
}
companion object {
private const val testClassPackage = "org.jetbrains.kotlin.android.tests"
private const val testClassName = "CodegenTestCaseOnAndroid"
private const val baseTestClassPackage = "org.jetbrains.kotlin.android.tests"
private const val baseTestClassName = "AbstractCodegenTestCaseOnAndroid"
private const val generatorName = "CodegenTestsOnAndroidGenerator"
const val testClassPackage = "org.jetbrains.kotlin.android.tests"
const val testClassName = "CodegenTestCaseOnAndroid"
const val baseTestClassPackage = "org.jetbrains.kotlin.android.tests"
const val baseTestClassName = "AbstractCodegenTestCaseOnAndroid"
const val generatorName = "CodegenTestsOnAndroidGenerator"
@JvmStatic
@Throws(Throwable::class)
fun generate(pathManager: PathManager) {
CodegenTestsOnAndroidGenerator(pathManager).generateOutputFiles()
CodegenTestsOnAndroidGenerator(pathManager).prepareAndroidModuleAndGenerateTests()
}
private fun hasBoxMethod(text: String): Boolean {
return text.contains("fun box()")
}
private fun generateTestMethod(p: Printer, testName: String, className: String, filePath: String) {
p.println("public void test$testName() throws Exception {")
p.pushIndent()
p.println("invokeBoxMethod($className.class, \"$filePath\", \"OK\");")
p.popIndent()
p.println("}")
p.println()
@Throws(IOException::class)
internal fun writeAndroidSkdToLocalProperties(pathManager: PathManager) {
val sdkRoot = File(pathManager.androidSdkRoot).invariantSeparatorsPath
println("Writing android sdk to local.properties: $sdkRoot")
val file = File(pathManager.tmpFolder + "/local.properties")
FileWriter(file).use { fw -> fw.write("sdk.dir=$sdkRoot") }
}
@JvmStatic
fun main(args: Array<String>) {
val tmpFolder = createTempDir()
println("Created temporary folder for android tests: " + tmpFolder.absolutePath)
val rootFolder = File("")
val pathManager = PathManager(rootFolder.absolutePath, tmpFolder.absolutePath)
generate(pathManager)
}
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.android.tests
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.utils.Printer
import java.io.File
import java.io.FileWriter
class TestInfo(val name: String, val fqName: FqName, val file: File)
class UnitTestFileWriter(private val fileName: String, private val index: Int, private val generatedTestNames: MutableSet<String>) {
private val infos = arrayListOf<TestInfo>()
fun addTests(info: List<TestInfo>) {
infos.addAll(info)
}
fun generate() {
FileWriter(File(fileName).also { it.parentFile.mkdirs() }).use { suite ->
val p = Printer(suite)
p.println(
"""package ${CodegenTestsOnAndroidGenerator.testClassPackage};
|
|import ${CodegenTestsOnAndroidGenerator.baseTestClassPackage}.${CodegenTestsOnAndroidGenerator.baseTestClassName};
|
|/* This class is generated by ${CodegenTestsOnAndroidGenerator.generatorName}. DO NOT MODIFY MANUALLY */
|public class ${CodegenTestsOnAndroidGenerator.testClassName}$index extends ${CodegenTestsOnAndroidGenerator.baseTestClassName} {
|
""".trimMargin()
)
p.pushIndent()
infos.forEach { info ->
generateTestMethod(
p,
generateTestName(info.file.name),
info.fqName.asString(),
StringUtil.escapeStringCharacters(info.file.path)
)
}
p.popIndent()
p.println("}")
}
}
private fun generateTestName(fileName: String): String {
var result = NameUtils.sanitizeAsJavaIdentifier(FileUtil.getNameWithoutExtension(StringUtil.capitalize(fileName)))
var i = 0
while (generatedTestNames.contains(result)) {
result += "_" + i++
}
generatedTestNames.add(result)
return result
}
private fun generateTestMethod(p: Printer, testName: String, className: String, filePath: String) {
p.println("public void test$testName() throws Exception {")
p.pushIndent()
p.println("invokeBoxMethod($className.class, \"$filePath\", \"OK\");")
p.popIndent()
p.println("}")
p.println()
}
}