[Native] New test infrastructure for black box tests

This commit is contained in:
Dmitriy Dolovov
2021-11-09 11:33:28 +03:00
parent b40592fcf3
commit 5f25b66eec
56 changed files with 55471 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
import org.jetbrains.kotlin.ideaExt.idea
plugins {
kotlin("jvm")
id("jps-compatible")
}
project.configureJvmToolchain(JdkMajorVersion.JDK_11)
dependencies {
testImplementation(kotlinStdlib())
testImplementation(project(":kotlin-reflect"))
testImplementation(intellijCoreDep()) { includeJars("intellij-core") }
testImplementation(intellijPluginDep("java"))
testImplementation(project(":kotlin-compiler-runner-unshaded"))
testImplementation(projectTests(":compiler:tests-common"))
testImplementation(projectTests(":compiler:tests-common-new"))
testImplementation(projectTests(":compiler:test-infrastructure"))
testImplementation(projectTests(":generators:test-generator"))
testApiJUnit5()
testRuntimeOnly(intellijDep()) { includeJars("trove4j", "intellij-deps-fastutil-8.4.1-4") }
}
val generationRoot = projectDir.resolve("tests-gen")
val extGenerationRoot = projectDir.resolve("ext-tests-gen")
sourceSets {
"main" { none() }
"test" {
projectDefault()
java.srcDirs(generationRoot.name, extGenerationRoot.name)
}
}
if (kotlinBuildProperties.isInJpsBuildIdeaSync) {
apply(plugin = "idea")
idea {
module.generatedSourceDirs.addAll(listOf(generationRoot, extGenerationRoot))
}
}
if (kotlinBuildProperties.isKotlinNativeEnabled) {
val kotlinNativeHome = project(":kotlin-native").projectDir.resolve("dist")
val kotlinNativeCompilerClassPath: Configuration by configurations.creating
dependencies {
kotlinNativeCompilerClassPath(project(":kotlin-native-compiler-embeddable"))
}
projectTest(taskName = "test", jUnitMode = JUnitMode.JUnit5) {
dependsOn(":kotlin-native:dist" /*, ":kotlin-native:distPlatformLibs"*/)
workingDir = rootDir
maxHeapSize = "6G" // Extra heap space for Kotlin/Native compiler.
jvmArgs("-XX:MaxJavaStackTraceDepth=1000000") // Effectively remove the limit for the amount of stack trace elements in Throwable.
// Double the stack size. This is needed to compile some marginal tests with extra-deep IR tree, which requires a lot of stack frames
// for visiting it. Example: codegen/box/strings/concatDynamicWithConstants.kt
// Such tests are successfully compiled in old test infra with the default 1 MB stack just by accident. New test infra requires ~55
// additional stack frames more compared to the old one because of another launcher, etc. and it turns out this is not enough.
jvmArgs("-Xss2m")
systemProperty("kotlin.native.home", kotlinNativeHome.absolutePath)
systemProperty("kotlin.internal.native.classpath", kotlinNativeCompilerClassPath.files.joinToString(";"))
// Pass Gradle properties as JVM properties so test process can read them.
listOf(
"kotlin.internal.native.test.mode",
"kotlin.internal.native.test.useCache"
).forEach { propertyName -> findProperty(propertyName)?.let { systemProperty(propertyName, it) } }
useJUnitPlatform()
}
} else {
getOrCreateTask<Test>(taskName = "test") {
doFirst {
throw GradleException(
"""
Can't run Kotlin/Native tests. The Kotlin/Native part of the project is currently disabled.
Make sure that "kotlin.native.enabled" is set to "true" in local.properties file,
or is passed as a Gradle command-line parameter via "-Pkotlin.native.enabled=true".
""".trimIndent()
)
}
}
}
val generateOwnTests by generator("org.jetbrains.kotlin.generators.tests.GenerateNativeBlackboxTestsKt") {
javaLauncher.set(project.getToolchainLauncherFor(JdkMajorVersion.JDK_11))
}
val generateExtTests by generator("org.jetbrains.kotlin.generators.tests.GenerateExtNativeBlackboxTestsKt") {
javaLauncher.set(project.getToolchainLauncherFor(JdkMajorVersion.JDK_11))
dependsOn(":compiler:generateTestData")
}
val generateTests by tasks.creating<Task> {
dependsOn(generateOwnTests, generateExtTests)
}
+4
View File
@@ -0,0 +1,4 @@
42
42
42
42
+1
View File
@@ -0,0 +1 @@
Hello, Kotlin!
+1
View File
@@ -0,0 +1 @@
Hello, Kotlin!
@@ -0,0 +1,21 @@
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,23 @@
// FREE_COMPILER_ARGS: -opt -verbose
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,31 @@
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,39 @@
// FILE: addition.kt
package samples.regular_multifile_with_explicit_packages
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// FILE: multiplication.kt
package samples.regular_multifile_with_explicit_packages.a
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// FILE: subtraction.kt
package samples.regular_multifile_with_explicit_packages.b
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// FILE: division.kt
package samples.regular_multifile_with_explicit_packages.a.b
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,35 @@
// MODULE: a
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// MODULE: m(a)
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// MODULE: s(m)()
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// MODULE: d(s)(s)
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,51 @@
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// MODULE: b(default)
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// MODULE: c(b)
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
// MODULE: d(default)
// FILE: division2.kt
import kotlin.test.*
@Test
fun division2 () {
assertEquals(42, 126 / 3)
}
// MODULE: e(d)
// FILE: division3.kt
import kotlin.test.*
@Test
fun division3 () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,37 @@
/**
* This is a header comment that should be prepended to the addition.kt file.
*/
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// MODULE: b(default)
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// MODULE: c(default)(default)
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,40 @@
import kotlin.test.*
@Test
fun foo() {
println("This is a statement that should go to main.kt in the default module")
}
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// MODULE: b(default)
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// MODULE: c(default)(default)
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,39 @@
/**
* This is a header comment that should be prepended to the addition.kt file.
*/
// MODULE: a
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// MODULE: m(a)
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// MODULE: s(m)()
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// MODULE: d(s)(s)
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,42 @@
import kotlin.test.*
@Test
fun foo() {
println("This is a statement that should go to main.kt in the default module")
}
// MODULE: a
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// MODULE: m(a)
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// MODULE: s(m)()
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// MODULE: d(s)(s)
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,21 @@
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,22 @@
// KIND: REGULAR
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,21 @@
import kotlin.test.*
@Test
fun addition() {
println(40 + 2)
}
@Test
fun multiplication () {
println(21 * 2)
}
@Test
fun subtraction () {
println(50 - 8)
}
@Test
fun division () {
println(126 / 3)
}
@@ -0,0 +1,33 @@
// KIND: STANDALONE
// FILE: addition.kt
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
// FILE: multiplication.kt
import kotlin.test.*
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
// FILE: subtraction.kt
import kotlin.test.*
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
// FILE: division.kt
import kotlin.test.*
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,21 @@
// KIND: STANDALONE_NO_TR
// ENTRY_POINT: foo.bar.altMain
// FILE: main.kt
package foo.bar.baz.qux
fun main() {
error("Not supposed to be called")
}
// FILE: altMain.kt
package foo.bar
import kotlin.test.*
fun altMain() {
assertEquals(42, 40 + 2)
println("OK")
}
@@ -0,0 +1,12 @@
// KIND: STANDALONE_NO_TR
import kotlin.test.*
fun main() {
assertEquals(42, 40 + 2)
println("OK")
}
fun altMain() {
error("Not supposed to be called")
}
@@ -0,0 +1,12 @@
// KIND: STANDALONE_NO_TR
import kotlin.test.*
fun main(args: Array<String>) {
assertEquals(42, 40 + 2)
println("OK")
}
fun altMain(args: Array<String>) {
error("Not supposed to be called")
}
@@ -0,0 +1,15 @@
// KIND: STANDALONE_NO_TR
// ENTRY_POINT: foo.bar.altMain
package foo.bar
import kotlin.test.*
fun main() {
error("Not supposed to be called")
}
fun altMain() {
assertEquals(42, 40 + 2)
println("OK")
}
@@ -0,0 +1,15 @@
// KIND: STANDALONE_NO_TR
// ENTRY_POINT: foo.bar.altMain
package foo.bar
import kotlin.test.*
fun main(args: Array<String>) {
error("Not supposed to be called")
}
fun altMain(args: Array<String>) {
assertEquals(42, 40 + 2)
println("OK")
}
@@ -0,0 +1,7 @@
// KIND: STANDALONE_NO_TR
// INPUT_DATA_FILE: hello.in
// OUTPUT_DATA_FILE: hello.out
fun main() {
println(readLine()!!)
}
@@ -0,0 +1,25 @@
// KIND: STANDALONE_NO_TR
// OUTPUT_DATA_FILE: 4x42.out
fun main() {
addition()
multiplication()
subtraction()
division()
}
fun addition() {
println(40 + 2)
}
fun multiplication () {
println(21 * 2)
}
fun subtraction () {
println(50 - 8)
}
fun division () {
println(126 / 3)
}
@@ -0,0 +1,23 @@
// KIND: STANDALONE
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,24 @@
// KIND: STANDALONE
// OUTPUT_DATA_FILE: 4x42.out
import kotlin.test.*
@Test
fun addition() {
println(40 + 2)
}
@Test
fun multiplication () {
println(21 * 2)
}
@Test
fun subtraction () {
println(50 - 8)
}
@Test
fun division () {
println(126 / 3)
}
@@ -0,0 +1,21 @@
import kotlin.test.*
@Test
fun addition() {
assertEquals(42, 40 + 2)
}
@Test
fun multiplication () {
assertEquals(42, 21 * 2)
}
@Test
fun subtraction () {
assertEquals(42, 50 - 8)
}
@Test
fun division () {
assertEquals(42, 126 / 3)
}
@@ -0,0 +1,193 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link GenerateNewCompilerTests.kt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
public class NativeBlackBoxTestGenerated extends AbstractNativeBlackBoxTest {
@Nested
@TestMetadata("native/tests-blackbox/testData/samples")
@TestDataPath("$PROJECT_ROOT")
public class Samples {
@Test
public void testAllFilesPresentInSamples() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("native/tests-blackbox/testData/samples"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@Test
@TestMetadata("regular_custom_args.kt")
public void testRegular_custom_args() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_custom_args.kt");
}
@Test
@TestMetadata("regular_multifile.kt")
public void testRegular_multifile() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multifile.kt");
}
@Test
@TestMetadata("regular_multifile_with_explicit_packages.kt")
public void testRegular_multifile_with_explicit_packages() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multifile_with_explicit_packages.kt");
}
@Test
@TestMetadata("regular_multimodule.kt")
public void testRegular_multimodule() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multimodule.kt");
}
@Test
@TestMetadata("regular_multimodule_implicit_first_module.kt")
public void testRegular_multimodule_implicit_first_module() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multimodule_implicit_first_module.kt");
}
@Test
@TestMetadata("regular_multimodule_implicit_first_module_with_header_comment.kt")
public void testRegular_multimodule_implicit_first_module_with_header_comment() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multimodule_implicit_first_module_with_header_comment.kt");
}
@Test
@TestMetadata("regular_multimodule_implicit_first_module_with_header_statement.kt")
public void testRegular_multimodule_implicit_first_module_with_header_statement() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multimodule_implicit_first_module_with_header_statement.kt");
}
@Test
@TestMetadata("regular_multimodule_with_header_comment.kt")
public void testRegular_multimodule_with_header_comment() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multimodule_with_header_comment.kt");
}
@Test
@TestMetadata("regular_multimodule_with_header_statement.kt")
public void testRegular_multimodule_with_header_statement() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_multimodule_with_header_statement.kt");
}
@Test
@TestMetadata("regular_simple.kt")
public void testRegular_simple() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_simple.kt");
}
@Test
@TestMetadata("regular_simple_explicit_kind.kt")
public void testRegular_simple_explicit_kind() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_simple_explicit_kind.kt");
}
@Test
@TestMetadata("regular_simple_with_output.kt")
public void testRegular_simple_with_output() throws Exception {
runTest("native/tests-blackbox/testData/samples/regular_simple_with_output.kt");
}
@Test
@TestMetadata("standalone_multifile.kt")
public void testStandalone_multifile() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_multifile.kt");
}
@Test
@TestMetadata("standalone_notr_multifile_entry_point.kt")
public void testStandalone_notr_multifile_entry_point() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_notr_multifile_entry_point.kt");
}
@Test
@TestMetadata("standalone_notr_simple.kt")
public void testStandalone_notr_simple() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_notr_simple.kt");
}
@Test
@TestMetadata("standalone_notr_simple2.kt")
public void testStandalone_notr_simple2() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_notr_simple2.kt");
}
@Test
@TestMetadata("standalone_notr_simple_entry_point.kt")
public void testStandalone_notr_simple_entry_point() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_notr_simple_entry_point.kt");
}
@Test
@TestMetadata("standalone_notr_simple_entry_point2.kt")
public void testStandalone_notr_simple_entry_point2() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_notr_simple_entry_point2.kt");
}
@Test
@TestMetadata("standalone_notr_simple_with_input_and_output.kt")
public void testStandalone_notr_simple_with_input_and_output() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_notr_simple_with_input_and_output.kt");
}
@Test
@TestMetadata("standalone_notr_simple_with_output.kt")
public void testStandalone_notr_simple_with_output() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_notr_simple_with_output.kt");
}
@Test
@TestMetadata("standalone_simple.kt")
public void testStandalone_simple() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_simple.kt");
}
@Test
@TestMetadata("standalone_simple_with_output.kt")
public void testStandalone_simple_with_output() throws Exception {
runTest("native/tests-blackbox/testData/samples/standalone_simple_with_output.kt");
}
@Nested
@TestMetadata("native/tests-blackbox/testData/samples/inner")
@TestDataPath("$PROJECT_ROOT")
public class Inner {
@Test
public void testAllFilesPresentInInner() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("native/tests-blackbox/testData/samples/inner"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@Test
@TestMetadata("regular_simple.kt")
public void testRegular_simple() throws Exception {
runTest("native/tests-blackbox/testData/samples/inner/regular_simple.kt");
}
}
}
@Nested
@TestMetadata("native/tests-blackbox/testData/samples2")
@TestDataPath("$PROJECT_ROOT")
public class Samples2 {
@Test
public void testAllFilesPresentInSamples2() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("native/tests-blackbox/testData/samples2"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@Test
@TestMetadata("regular_simple.kt")
public void testRegular_simple() throws Exception {
runTest("native/tests-blackbox/testData/samples2/regular_simple.kt");
}
}
}
@@ -0,0 +1,11 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.generators.tests
import kotlin.reflect.KClass
@Target(AnnotationTarget.CLASS)
annotation class CustomNativeBlackBoxTestCaseGroupProvider(val value: KClass<*>)
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.generators.tests
import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5
import org.jetbrains.kotlin.generators.model.annotation
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeBlackBoxTest
import org.jetbrains.kotlin.konan.blackboxtest.group.ExtTestCaseGroupProvider
fun main() {
System.setProperty("java.awt.headless", "true")
runAndLogDuration("Generating external Kotlin/Native blackbox tests") {
generateTestGroupSuiteWithJUnit5 {
cleanTestGroup(
testsRoot = "native/tests-blackbox/ext-tests-gen",
testDataRoot = "compiler/testData"
) {
testClass<AbstractNativeBlackBoxTest>(
suiteTestClassName = "NativeExtBlackBoxTestGenerated",
annotations = listOf(annotation(CustomNativeBlackBoxTestCaseGroupProvider::class.java, ExtTestCaseGroupProvider::class.java))
) {
model("codegen/box")
model("codegen/boxInline")
}
}
}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.generators.tests
import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeBlackBoxTest
fun main() {
System.setProperty("java.awt.headless", "true")
runAndLogDuration("Generating Kotlin/Native blackbox tests") {
generateTestGroupSuiteWithJUnit5 {
cleanTestGroup("native/tests-blackbox/tests-gen", "native/tests-blackbox/testData") {
testClass<AbstractNativeBlackBoxTest> {
model("samples")
model("samples2")
}
}
}
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package org.jetbrains.kotlin.generators.tests
import org.jetbrains.kotlin.generators.TestGroup
import org.jetbrains.kotlin.generators.TestGroupSuite
import org.jetbrains.kotlin.konan.blackboxtest.util.getAbsoluteFile
import java.io.File
import kotlin.system.measureTimeMillis
/** Fully clean-up the tests root directory before generating new test classes. */
internal fun TestGroupSuite.cleanTestGroup(testsRoot: String, testDataRoot: String, init: TestGroup.() -> Unit) {
getAbsoluteFile(localPath = testsRoot).deleteRecursivelyWithLogging()
testGroup(testsRoot, testDataRoot, init = init)
}
internal fun runAndLogDuration(actionDescription: String, action: () -> Unit) {
println("$actionDescription...")
val duration = measureTimeMillis(action)
val seconds = duration / 1000
val millis = duration % 100
println(
buildString {
append(actionDescription)
append(" took ")
if (seconds > 0) append(seconds).append("s ")
append(millis).append("ms")
}
)
}
internal fun File.deleteRecursivelyWithLogging() {
if (exists()) {
walkBottomUp().forEach { entry ->
val message = when {
entry.isFile -> "File removed: $entry"
entry.isDirectory && entry == this -> {
// Don't report directories except for the root directory.
"Directory removed (recursively): $entry"
}
else -> null
}
entry.delete()
message?.let(::println)
}
}
}
@@ -0,0 +1,19 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.konan.blackboxtest.util.getAbsoluteFile
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(NativeBlackBoxTestSupport::class)
abstract class AbstractNativeBlackBoxTest {
internal lateinit var blackBoxTestProvider: TestProvider
fun runTest(@TestDataFile testDataFilePath: String) {
blackBoxTestProvider.getTestByTestDataFile(getAbsoluteFile(testDataFilePath)).runAndVerify()
}
}
@@ -0,0 +1,173 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.cli.common.ExitCode
import java.io.File
/**
* A piece of information that makes sense for the user, and that can be logged to a file or
* displayed inside an error message in user-friendly way.
*
* Handles all the necessary formatting right inside of [computeText]. Caches the resulting text to avoid re-computation.
*/
internal abstract class LoggedData {
private val text: String by lazy { computeText() }
protected abstract fun computeText(): String
final override fun toString() = text
class CompilerParameters(
private val compilerArgs: Array<String>,
private val sourceModules: Collection<TestModule>
) : LoggedData() {
private val testDataFiles: List<File>
get() = sourceModules.asSequence()
.filterIsInstance<TestModule.Exclusive>()
.map { it.testCase.origin.testDataFile }
.toMutableList()
.apply { sort() }
override fun computeText() = buildString {
appendArguments("COMPILER ARGUMENTS:", listOf("\$\$kotlinc-native\$\$") + compilerArgs)
appendLine()
appendList("TEST DATA FILES (COMPILED TOGETHER):", testDataFiles)
}
}
class CompilerCall(
private val parameters: CompilerParameters,
private val exitCode: ExitCode,
private val compilerOutput: String,
private val compilerOutputHasErrors: Boolean,
private val durationMillis: Long
) : LoggedData() {
override fun computeText(): String {
val problems = listOfNotNull(
"- Non-zero exit code".takeIf { exitCode != ExitCode.OK },
"- Errors reported by the compiler".takeIf { compilerOutputHasErrors }
)
return buildString {
if (problems.isNotEmpty()) {
appendLine("COMPILATION PROBLEMS:")
problems.forEach(::appendLine)
appendLine()
}
appendLine("COMPILER CALL:")
appendLine("- Exit code: ${exitCode.code} (${exitCode.name})")
appendDuration(durationMillis)
appendLine()
appendLine("========== BEGIN: RAW COMPILER OUTPUT ==========")
if (compilerOutput.isNotEmpty()) appendLine(compilerOutput.trimEnd())
appendLine("========== END: RAW COMPILER OUTPUT ==========")
appendLine()
appendLine(parameters)
}
}
}
class CompilerCallUnexpectedFailure(
private val parameters: CompilerParameters,
private val throwable: Throwable
) : LoggedData() {
override fun computeText() = buildString {
appendLine("UNEXPECTED FAILURE IN COMPILER:")
appendLine("$throwable")
appendLine()
appendLine("STACK TRACE:")
appendLine(throwable.stackTraceToString())
appendLine()
appendLine(parameters)
}
}
class TestRunParameters(
private val compilerCall: CompilerCall,
private val origin: TestOrigin.SingleTestDataFile,
private val runArgs: Iterable<String>,
private val runParameters: List<TestRunParameter>
) : LoggedData() {
override fun computeText() = buildString {
appendLine("TEST DATA FILE:")
appendLine(origin.testDataFile)
appendLine()
appendArguments("TEST RUN ARGUMENTS:", runArgs)
appendLine()
runParameters.get<TestRunParameter.WithInputData> {
appendLine("INPUT DATA FILE:")
appendLine(inputDataFile)
appendLine()
}
runParameters.get<TestRunParameter.WithExpectedOutputData> {
appendLine("EXPECTED OUTPUT DATA FILE:")
appendLine(expectedOutputDataFile)
appendLine()
}
appendLine(compilerCall)
}
}
class TestRun(
private val parameters: TestRunParameters,
private val exitCode: Int,
private val stdOut: String,
private val stdErr: String,
private val durationMillis: Long
) : LoggedData() {
override fun computeText() = buildString {
appendLine("TEST RUN:")
appendLine("- Exit code: $exitCode")
appendDuration(durationMillis)
appendLine()
appendLine("========== BEGIN: TEST STDOUT ==========")
if (stdOut.isNotEmpty()) appendLine(stdOut.trimEnd())
appendLine("========== END: TEST STDOUT ==========")
appendLine()
appendLine("========== BEGIN: TEST STDERR ==========")
if (stdErr.isNotEmpty()) appendLine(stdErr.trimEnd())
appendLine("========== END: TEST STDERR ==========")
appendLine()
appendLine(parameters)
}
}
companion object {
@JvmStatic
protected fun StringBuilder.appendList(header: String, list: Iterable<Any?>): StringBuilder {
appendLine(header)
list.forEach(::appendLine)
return this
}
@JvmStatic
protected fun StringBuilder.appendArguments(header: String, args: Iterable<String>): StringBuilder {
appendLine(header)
fun String.sanitize() = if (startsWith("--ktest_filter")) "'$this'" else this
var lastArgIsOptionWithoutEqualsSign = false
args.forEachIndexed { index, arg ->
val isOption = arg[0] == '-'
val isSourceFile = !isOption && arg.substringAfterLast('.') == "kt"
if (index > 0) {
if (isOption || isSourceFile || !lastArgIsOptionWithoutEqualsSign)
append(" \\\n")
else
append(' ')
}
lastArgIsOptionWithoutEqualsSign = isOption && '=' !in arg
append(arg.sanitize())
}
appendLine()
return this
}
protected fun StringBuilder.appendDuration(durationMillis: Long): StringBuilder =
append("- Duration: ").append(String.format("%.2f", durationMillis.toDouble() / 1000)).appendLine(" seconds")
}
}
@@ -0,0 +1,131 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.generators.tests.CustomNativeBlackBoxTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.group.StandardTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.group.TestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.util.*
import org.jetbrains.kotlin.test.TestMetadata
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.TestInstancePostProcessor
import java.io.File
import kotlin.reflect.KClass
class NativeBlackBoxTestSupport : BeforeEachCallback {
/**
* Note: [BeforeEachCallback.beforeEach] allows accessing test instances while [BeforeAllCallback.beforeAll] which may look
* more preferable here does not allow it because it is called at the time when test instances are not created yet.
* Also, [TestInstancePostProcessor.postProcessTestInstance] allows accessing only the currently created test instance and does
* not allow accessing its parent test instance in case there are inner test classes in the generated test suite.
*/
override fun beforeEach(extensionContext: ExtensionContext) = with(extensionContext) {
enclosingTestInstance.blackBoxTestProvider = getOrCreateBlackBoxTestProvider()
}
companion object {
private val NAMESPACE = ExtensionContext.Namespace.create(NativeBlackBoxTestSupport::class.java.simpleName)
/** Creates a single instance of [TestProvider] per test class. */
private fun ExtensionContext.getOrCreateBlackBoxTestProvider(): TestProvider =
root.getStore(NAMESPACE).getOrComputeIfAbsent(enclosingTestClass.sanitizedName) { sanitizedName ->
val globalEnvironment = getOrCreateGlobalEnvironment()
val testRoots = computeTestRoots()
val testSourcesDir = globalEnvironment.baseBuildDir
.resolve("blackbox-test-sources")
.resolve(sanitizedName)
.ensureExistsAndIsEmptyDirectory() // Clean-up the directory with all potentially stale generated sources.
val sharedSourcesDir = testSourcesDir
.resolve("__shared_modules__")
.ensureExistsAndIsEmptyDirectory()
val testBinariesDir = globalEnvironment.baseBuildDir
.resolve("blackbox-test-binaries")
.resolve(globalEnvironment.target.name)
.resolve(sanitizedName)
.ensureExistsAndIsEmptyDirectory() // Clean-up the directory with all potentially stale artifacts.
val sharedBinariesDir = testBinariesDir
.resolve("__shared_modules__")
.ensureExistsAndIsEmptyDirectory()
val environment = TestEnvironment(
globalEnvironment = globalEnvironment,
testRoots = testRoots,
testSourcesDir = testSourcesDir,
sharedSourcesDir = sharedSourcesDir,
testBinariesDir = testBinariesDir,
sharedBinariesDir = sharedBinariesDir
)
val testCaseGroupProvider = requiredTestClass.createTestCaseGroupProvider(environment)
TestProvider(environment, testCaseGroupProvider)
}.cast()
private fun ExtensionContext.getOrCreateGlobalEnvironment(): GlobalTestEnvironment =
root.getStore(NAMESPACE).getOrComputeIfAbsent(GlobalTestEnvironment::class.java.sanitizedName) {
// Create with the default settings.
GlobalTestEnvironment()
}.cast()
private val ExtensionContext.enclosingTestInstance: AbstractNativeBlackBoxTest
get() = requiredTestInstances.allInstances.firstOrNull().cast()
private val ExtensionContext.enclosingTestClass: Class<*>
get() = generateSequence(requiredTestClass) { it.enclosingClass }.last()
private fun ExtensionContext.computeTestRoots(): TestRoots {
val enclosingTestClass = enclosingTestClass
val testRoots: Set<File> = when (val outermostTestMetadata = enclosingTestClass.getAnnotation(TestMetadata::class.java)) {
null -> {
enclosingTestClass.declaredClasses.mapNotNullToSet { nestedClass ->
nestedClass.getAnnotation(TestMetadata::class.java)?.testRoot
}
}
else -> setOf(outermostTestMetadata.testRoot)
}
val baseDir: File = when (testRoots.size) {
0 -> fail { "No test roots found for $enclosingTestClass test class." }
1 -> testRoots.first().parentFile
else -> {
val baseDirs = testRoots.mapToSet { it.parentFile }
assertEquals(1, baseDirs.size) {
"Controversial base directories computed for test roots for $enclosingTestClass test class: $baseDirs"
}
baseDirs.first()
}
}
return TestRoots(testRoots, baseDir)
}
private fun Class<*>.createTestCaseGroupProvider(environment: TestEnvironment): TestCaseGroupProvider {
val providerClass = getAnnotation(CustomNativeBlackBoxTestCaseGroupProvider::class.java)?.value
?: return StandardTestCaseGroupProvider(environment)
val constructor = providerClass.constructors.firstOrNull { constructor ->
val singleParameter = constructor.parameters.singleOrNull() ?: return@firstOrNull false
(singleParameter.type.classifier as? KClass<*>)?.qualifiedName == TestEnvironment::class.qualifiedName
} ?: fail { "No suitable constructor for $providerClass" }
return constructor.call(environment).cast()
}
private val TestMetadata.testRoot: File get() = getAbsoluteFile(localPath = value)
}
}
@@ -0,0 +1,242 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.konan.blackboxtest.TestModule.Companion.allDependencies
import org.jetbrains.kotlin.konan.blackboxtest.util.*
import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.File
internal typealias PackageName = String
/**
* Helps to track the origin of every [TestCase], [TestCompilation] or [TestExecutable]. Used for issue reporting purposes.
*/
internal interface TestOrigin {
class SingleTestDataFile(val testDataFile: File) : TestOrigin {
override fun toString(): String = testDataFile.path
}
}
/**
* Represents a single file that will be supplied to the compiler.
*/
internal class TestFile<M : TestModule> private constructor(
val location: File,
val module: M,
private var state: State
) {
private sealed interface State {
object Committed : State
class Uncommitted(var text: String) : State
}
fun update(transformation: (String) -> String) {
when (val state = state) {
is State.Uncommitted -> state.text = transformation(state.text)
is State.Committed -> fail { "File $location is already committed." }
}
}
// An optimization to release the memory occupied by numerous file texts.
fun commit() {
state = when (val state = state) {
is State.Uncommitted -> {
location.parentFile.mkdirs()
location.writeText(state.text)
State.Committed
}
is State.Committed -> state
}
}
override fun equals(other: Any?) = other === this || (other as? TestFile<*>)?.location?.path == location.path
override fun hashCode() = location.path.hashCode()
override fun toString() = "TestFile(location=$location, module.name=${module.name}, state=${state::class.java.simpleName})"
companion object {
fun <M : TestModule> createUncommitted(location: File, module: M, text: CharSequence) =
TestFile(location, module, State.Uncommitted(text.toString()))
fun <M : TestModule> createCommitted(location: File, module: M) =
TestFile(location, module, State.Committed)
}
}
/**
* One or more [TestFile]s that are always compiled together.
*
* Please note that [TestModule] is the minimal possible compilation unit, but not always the maximal possible compilation unit.
* In certain test modes (ex: [TestMode.ONE_STAGE], [TestMode.TWO_STAGE]) modules represented by [TestModule] are ignored, and
* all [TestFile]s are compiled together in one shot.
*
* [TestModule.Exclusive] represents a collection of [TestFile]s used exclusively for an individual [TestCase].
* [TestModule.Shared] represents a "shared" module, i.e. the auxiliary module that can be used in multiple [TestCase]s.
*/
internal sealed class TestModule {
abstract val name: String
abstract val files: Set<TestFile<*>>
data class Exclusive(
override val name: String,
val directDependencySymbols: Set<String>,
val directFriendSymbols: Set<String>
) : TestModule() {
override val files: FailOnDuplicatesSet<TestFile<Exclusive>> = FailOnDuplicatesSet()
lateinit var directDependencies: Set<TestModule>
lateinit var directFriends: Set<TestModule>
// N.B. The following two properties throw an exception on attempt to resolve cyclic dependencies.
val allDependencies: Set<TestModule> by SM.lazyNeighbors({ directDependencies }, { it.allDependencies })
val allFriends: Set<TestModule> by SM.lazyNeighbors({ directFriends }, { it.allFriends })
lateinit var testCase: TestCase
fun commit() {
files.forEach { it.commit() }
}
fun haveSameSymbols(other: Exclusive) =
other.directDependencySymbols == directDependencySymbols && other.directFriendSymbols == directFriendSymbols
}
class Shared(override val name: String) : TestModule() {
override val files: FailOnDuplicatesSet<TestFile<Shared>> = FailOnDuplicatesSet()
}
final override fun equals(other: Any?) =
other === this || (other is TestModule && other.javaClass == javaClass && other.name == name && other.files == files)
final override fun hashCode() = (javaClass.hashCode() * 31 + name.hashCode()) * 31 + files.hashCode()
final override fun toString() = "${javaClass.canonicalName}[name=$name]"
companion object {
fun newDefaultModule() = Exclusive(DEFAULT_MODULE_NAME, emptySet(), emptySet())
val TestModule.allDependencies: Set<TestModule>
get() = when (this) {
is Exclusive -> allDependencies
is Shared -> emptySet()
}
val TestModule.allFriends: Set<TestModule>
get() = when (this) {
is Exclusive -> allFriends
is Shared -> emptySet()
}
private val SM = LockBasedStorageManager(TestModule::class.java.name)
}
}
/**
* A minimal testable unit.
*
* [modules] - the collection of [TestModule.Exclusive] modules with [TestFile]s that need to be compiled to run this test.
* Note: There can also be [TestModule.Shared] modules as dependencies of either of [TestModule.Exclusive] modules.
* See [TestModule.Exclusive.allDependencies] for details.
* [origin] - the origin of the test case.
* [nominalPackageName] - the unique package name that was computed for this [TestCase] based on [origin]'s actual path.
* Note: It depends on the concrete [TestKind] whether the package name will be enforced for the [TestFile]s or not.
*/
internal class TestCase(
val kind: TestKind,
val modules: Set<TestModule.Exclusive>,
val freeCompilerArgs: TestCompilerArgs,
val origin: TestOrigin.SingleTestDataFile,
val nominalPackageName: PackageName,
val expectedOutputDataFile: File?,
val extras: StandaloneNoTestRunnerExtras? = null
) {
// The set of module that have no incoming dependency arcs.
val rootModules: Set<TestModule.Exclusive> by lazy {
val allModules = hashSetOf<TestModule>()
modules.forEach { module ->
allModules += module
allModules += module.allDependencies
}
val rootModules = allModules.toHashSet()
allModules.forEach { module ->
rootModules -= module.allDependencies
}
assertTrue(rootModules.isNotEmpty()) { "No root modules in test case. Origin: $origin." }
val nonExclusiveRootTestModules = rootModules.filter { module -> module !is TestModule.Exclusive }
assertTrue(nonExclusiveRootTestModules.isEmpty()) {
"There are non-exclusive root test modules in test case. Origin: $origin. Modules: $nonExclusiveRootTestModules"
}
@Suppress("UNCHECKED_CAST")
rootModules as Set<TestModule.Exclusive>
}
class StandaloneNoTestRunnerExtras(val entryPoint: String, val inputDataFile: File?)
init {
assertEquals(extras != null, kind == TestKind.STANDALONE_NO_TR)
}
fun initialize(findSharedModule: ((moduleName: String) -> TestModule.Shared?)?) {
// Check that there are no duplicated files among different modules.
val duplicatedFiles = modules.flatMap { it.files }.groupingBy { it }.eachCount().filterValues { it > 1 }.keys
assertTrue(duplicatedFiles.isEmpty()) { "$origin: Duplicated test files encountered: $duplicatedFiles" }
// Check that there are modules with duplicated names.
val exclusiveModules: Map</* regular module name */ String, TestModule.Exclusive> = modules.toIdentitySet()
.groupingBy { module -> module.name }
.aggregate { moduleName, _: TestModule.Exclusive?, module, isFirst ->
assertTrue(isFirst) { "$origin: Multiple test modules with the same name found: $moduleName" }
module
}
fun findModule(moduleName: String): TestModule = exclusiveModules[moduleName]
?: findSharedModule?.invoke(moduleName)
?: fail { "$origin: Module $moduleName not found" }
modules.forEach { module ->
module.commit() // Save to the file system and release the memory.
module.testCase = this
module.directDependencies = module.directDependencySymbols.mapToSet(::findModule)
module.directFriends = module.directFriendSymbols.mapToSet(::findModule)
}
}
}
/**
* A group of [TestCase]s that were obtained from a particular testData directory.
*/
internal interface TestCaseGroup {
fun isEnabled(testDataFileName: String): Boolean
fun getByName(testDataFileName: String): TestCase?
fun getRegularOnlyByCompilerArgs(freeCompilerArgs: TestCompilerArgs): Collection<TestCase>
class Default(
private val disabledTestDataFileNames: Set<String>,
testCases: Iterable<TestCase>
) : TestCaseGroup {
private val testCasesByTestDataFileNames = testCases.associateBy { it.origin.testDataFile.name }
override fun isEnabled(testDataFileName: String) = testDataFileName !in disabledTestDataFileNames
override fun getByName(testDataFileName: String) = testCasesByTestDataFileNames[testDataFileName]
override fun getRegularOnlyByCompilerArgs(freeCompilerArgs: TestCompilerArgs) =
testCasesByTestDataFileNames.values.filter { it.kind == TestKind.REGULAR && it.freeCompilerArgs == freeCompilerArgs }
}
companion object {
val ALL_DISABLED = object : TestCaseGroup {
override fun isEnabled(testDataFileName: String) = false
override fun getByName(testDataFileName: String) = error("This function should not be called")
override fun getRegularOnlyByCompilerArgs(freeCompilerArgs: TestCompilerArgs) = error("This function should not be called")
}
}
}
@@ -0,0 +1,384 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl
import org.jetbrains.kotlin.compilerRunner.processCompilerOutput
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.konan.blackboxtest.TestCompilation.Companion.resultingArtifactPath
import org.jetbrains.kotlin.konan.blackboxtest.TestCompilationResult.Companion.assertSuccess
import org.jetbrains.kotlin.konan.blackboxtest.TestModule.Companion.allDependencies
import org.jetbrains.kotlin.konan.blackboxtest.TestModule.Companion.allFriends
import org.jetbrains.kotlin.konan.blackboxtest.util.*
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.system.measureTimeMillis
internal class TestCompilationFactory(private val environment: TestEnvironment) {
private val cachedCompilations: MutableMap<TestCompilationCacheKey, TestCompilation> = ConcurrentHashMap()
private sealed interface TestCompilationCacheKey {
data class Klib(val sourceModules: Set<TestModule>, val freeCompilerArgs: TestCompilerArgs) : TestCompilationCacheKey
data class Executable(val sourceModules: Set<TestModule>) : TestCompilationCacheKey
}
fun testCasesToExecutable(testCases: Collection<TestCase>): TestCompilation {
val rootModules = testCases.flatMapToSet { testCase -> testCase.rootModules }
val cacheKey = TestCompilationCacheKey.Executable(rootModules)
// Fast pass.
cachedCompilations[cacheKey]?.let { return it }
// Long pass.
val freeCompilerArgs = rootModules.first().testCase.freeCompilerArgs
val libraries = rootModules.flatMapToSet { it.allDependencies }.map { moduleToKlib(it, freeCompilerArgs) }
val friends = rootModules.flatMapToSet { it.allFriends }.map { moduleToKlib(it, freeCompilerArgs) }
return cachedCompilations.computeIfAbsent(cacheKey) {
val entryPoint = testCases.singleOrNull()?.extras?.entryPoint
TestCompilationImpl(
environment = environment,
freeCompilerArgs = freeCompilerArgs,
sourceModules = rootModules,
dependencies = TestCompilationDependencies(libraries = libraries, friends = friends),
expectedArtifactFile = artifactFileForExecutable(rootModules),
specificCompilerArgs = {
add("-produce", "program")
if (entryPoint != null) add("-entry", entryPoint) else add("-generate-test-runner")
environment.globalEnvironment.getRootCacheDirectory(debuggable = true)?.let { rootCacheDir ->
add("-Xcache-directory=$rootCacheDir")
}
}
)
}
}
private fun moduleToKlib(sourceModule: TestModule, freeCompilerArgs: TestCompilerArgs): TestCompilation {
val sourceModules = setOf(sourceModule)
val cacheKey = TestCompilationCacheKey.Klib(sourceModules, freeCompilerArgs)
// Fast pass.
cachedCompilations[cacheKey]?.let { return it }
// Long pass.
val libraries = sourceModule.allDependencies.map { moduleToKlib(it, freeCompilerArgs) }
val friends = sourceModule.allFriends.map { moduleToKlib(it, freeCompilerArgs) }
return cachedCompilations.computeIfAbsent(cacheKey) {
TestCompilationImpl(
environment = environment,
freeCompilerArgs = freeCompilerArgs,
sourceModules = sourceModules,
dependencies = TestCompilationDependencies(libraries = libraries, friends = friends),
expectedArtifactFile = artifactFileForKlib(sourceModule, freeCompilerArgs),
specificCompilerArgs = { add("-produce", "library") }
)
}
}
private fun artifactFileForExecutable(modules: Set<TestModule.Exclusive>) = when (modules.size) {
1 -> artifactFileForExecutable(modules.first())
else -> multiModuleArtifactFile(modules, environment.globalEnvironment.target.family.exeSuffix)
}
private fun artifactFileForExecutable(module: TestModule.Exclusive) =
singleModuleArtifactFile(module, environment.globalEnvironment.target.family.exeSuffix)
private fun artifactFileForKlib(module: TestModule, freeCompilerArgs: TestCompilerArgs) = when (module) {
is TestModule.Exclusive -> singleModuleArtifactFile(module, "klib")
is TestModule.Shared -> environment.sharedBinariesDir.resolve("${module.name}-${prettyHash(freeCompilerArgs.hashCode())}.klib")
}
private fun singleModuleArtifactFile(module: TestModule.Exclusive, extension: String): File {
val artifactFileName = buildString {
append(module.testCase.nominalPackageName.replace('.', '_')).append('.')
if (extension == "klib") append(module.name).append('.')
append(extension)
}
return artifactDirForPackageName(module.testCase.nominalPackageName).resolve(artifactFileName)
}
private fun multiModuleArtifactFile(modules: Collection<TestModule>, extension: String): File {
var filesCount = 0
var hash = 0
val uniquePackageNames = hashSetOf<PackageName>()
modules.forEach { module ->
module.files.forEach { file ->
filesCount++
hash = hash * 31 + file.hashCode()
}
if (module is TestModule.Exclusive)
uniquePackageNames += module.testCase.nominalPackageName
}
val commonPackageName = uniquePackageNames.findCommonPackageName()
val artifactFileName = buildString {
val prefix = filesCount.toString()
repeat(4 - prefix.length) { append('0') }
append(prefix).append('-')
if (commonPackageName != null)
append(commonPackageName.replace('.', '_')).append('-')
append(prettyHash(hash))
append('.').append(extension)
}
return artifactDirForPackageName(commonPackageName).resolve(artifactFileName)
}
private fun artifactDirForPackageName(packageName: PackageName?): File {
val baseDir = environment.testBinariesDir
val outputDir = if (packageName != null) baseDir.resolve(packageName.replace('.', '_')) else baseDir
outputDir.mkdirs()
return outputDir
}
}
internal interface TestCompilation {
val result: TestCompilationResult
companion object {
val TestCompilation.resultingArtifactPath: String
get() = result.assertSuccess().resultingArtifact.path
}
}
internal sealed interface TestCompilationResult {
sealed interface ImmediateResult : TestCompilationResult {
val loggedData: LoggedData
}
sealed interface Failure : ImmediateResult
data class Success(val resultingArtifact: File, override val loggedData: LoggedData.CompilerCall) : ImmediateResult
data class CompilerFailure(override val loggedData: LoggedData.CompilerCall) : Failure
data class UnexpectedFailure(override val loggedData: LoggedData.CompilerCallUnexpectedFailure) : Failure
data class DependencyFailures(val causes: Set<Failure>) : TestCompilationResult
companion object {
fun TestCompilationResult.assertSuccess(): Success = when (this) {
is Success -> this
is Failure -> fail { describeFailure() }
is DependencyFailures -> fail { describeDependencyFailures() }
}
val TestCompilationResult.resultingArtifact: File
get() = assertSuccess().resultingArtifact
private fun Failure.describeFailure() = when (this) {
is CompilerFailure -> "Compilation failed.\n\n$loggedData"
is UnexpectedFailure -> "Compilation failed with unexpected exception.\n\n$loggedData"
}
private fun DependencyFailures.describeDependencyFailures() =
buildString {
appendLine("Compilation aborted due to errors in dependency compilations (${causes.size} items). See details below.")
appendLine()
causes.forEachIndexed { index, cause ->
append("#").append(index + 1).append(". ")
appendLine(cause.describeFailure())
}
}
}
}
/**
* The dependencies of a particular [TestCompilationImpl].
*
* [libraries] - the [TestCompilation]s (modules) that should yield KLIBs to be consumed as dependency libraries in the current compilation.
* [friends] - similarly but friend modules (-friend-modules).
* [includedLibraries] - similarly but included modules (-Xinclude).
*/
internal class TestCompilationDependencies(
val libraries: Collection<TestCompilation> = emptyList(),
val friends: Collection<TestCompilation> = emptyList(),
val includedLibraries: Collection<TestCompilation> = emptyList()
) {
fun collectFailures(): Set<TestCompilationResult.Failure> = listOf(libraries, friends, includedLibraries)
.flatten()
.flatMapToSet { compilation ->
when (val result = compilation.result) {
is TestCompilationResult.Failure -> listOf(result)
is TestCompilationResult.DependencyFailures -> result.causes
is TestCompilationResult.Success -> emptyList()
}
}
}
private class TestCompilationImpl(
private val environment: TestEnvironment,
private val freeCompilerArgs: TestCompilerArgs,
private val sourceModules: Collection<TestModule>,
private val dependencies: TestCompilationDependencies,
private val expectedArtifactFile: File,
private val specificCompilerArgs: ArgsBuilder.() -> Unit
) : TestCompilation {
// Runs the compiler and memorizes the result on property access.
override val result: TestCompilationResult by lazy {
val failures = dependencies.collectFailures()
if (failures.isNotEmpty())
TestCompilationResult.DependencyFailures(causes = failures)
else
doCompile()
}
private fun ArgsBuilder.applyCommonArgs() {
add(
"-enable-assertions",
"-g",
"-target", environment.globalEnvironment.target.name,
"-repo", environment.globalEnvironment.kotlinNativeHome.resolve("klib").path,
"-output", expectedArtifactFile.path,
"-Xskip-prerelease-check",
"-Xverify-ir"
)
addFlattened(dependencies.libraries) { library -> listOf("-l", library.resultingArtifactPath) }
dependencies.friends.takeIf(Collection<*>::isNotEmpty)?.let { friends ->
add("-friend-modules", friends.joinToString(File.pathSeparator) { friend -> friend.resultingArtifactPath })
}
add(dependencies.includedLibraries) { include -> "-Xinclude=${include.resultingArtifactPath}" }
add(freeCompilerArgs.compilerArgs)
}
private fun ArgsBuilder.applySources() {
addFlattenedTwice(sourceModules, { it.files }) { it.location.path }
}
private fun doCompile(): TestCompilationResult.ImmediateResult {
val compilerArgs = buildArgs {
applyCommonArgs()
specificCompilerArgs()
applySources()
}
val loggedCompilerParameters = LoggedData.CompilerParameters(compilerArgs, sourceModules)
val (loggedCompilerCall: LoggedData, result: TestCompilationResult.ImmediateResult) = try {
val (exitCode, compilerOutput, compilerOutputHasErrors, durationMillis) = callCompiler(
compilerArgs = compilerArgs,
lazyKotlinNativeClassLoader = environment.globalEnvironment.lazyKotlinNativeClassLoader
)
val loggedCompilerCall =
LoggedData.CompilerCall(loggedCompilerParameters, exitCode, compilerOutput, compilerOutputHasErrors, durationMillis)
val result = if (exitCode != ExitCode.OK || compilerOutputHasErrors)
TestCompilationResult.CompilerFailure(loggedCompilerCall)
else
TestCompilationResult.Success(expectedArtifactFile, loggedCompilerCall)
loggedCompilerCall to result
} catch (unexpectedThrowable: Throwable) {
val loggedFailure = LoggedData.CompilerCallUnexpectedFailure(loggedCompilerParameters, unexpectedThrowable)
val result = TestCompilationResult.UnexpectedFailure(loggedFailure)
loggedFailure to result
}
getLogFile(expectedArtifactFile).writeText(loggedCompilerCall.toString())
return result
}
}
private class ArgsBuilder {
private val args = mutableListOf<String>()
fun add(vararg args: String) {
this.args += args
}
fun add(args: Iterable<String>) {
this.args += args
}
inline fun <T> add(rawArgs: Iterable<T>, transform: (T) -> String) {
rawArgs.mapTo(args) { transform(it) }
}
inline fun <T> addFlattened(rawArgs: Iterable<T>, transform: (T) -> Iterable<String>) {
rawArgs.flatMapTo(args) { transform(it) }
}
inline fun <T, R> addFlattenedTwice(rawArgs: Iterable<T>, transform1: (T) -> Iterable<R>, transform2: (R) -> String) {
rawArgs.forEach { add(transform1(it), transform2) }
}
fun build(): Array<String> = args.toTypedArray()
}
private inline fun buildArgs(builderAction: ArgsBuilder.() -> Unit): Array<String> {
return ArgsBuilder().apply(builderAction).build()
}
private fun callCompiler(compilerArgs: Array<String>, lazyKotlinNativeClassLoader: Lazy<ClassLoader>): CompilerCallResult {
val compilerXmlOutput: ByteArrayOutputStream
val exitCode: ExitCode
val durationMillis = measureTimeMillis {
val kotlinNativeClassLoader by lazyKotlinNativeClassLoader
val servicesClass = Class.forName(Services::class.java.canonicalName, true, kotlinNativeClassLoader)
val emptyServices = servicesClass.getField("EMPTY").get(servicesClass)
val compilerClass = Class.forName("org.jetbrains.kotlin.cli.bc.K2Native", true, kotlinNativeClassLoader)
val entryPoint = compilerClass.getMethod(
"execAndOutputXml",
PrintStream::class.java,
servicesClass,
Array<String>::class.java
)
compilerXmlOutput = ByteArrayOutputStream()
exitCode = PrintStream(compilerXmlOutput).use { printStream ->
val result = entryPoint.invoke(compilerClass.getDeclaredConstructor().newInstance(), printStream, emptyServices, compilerArgs)
ExitCode.valueOf(result.toString())
}
}
val messageCollector: MessageCollector
val compilerOutput: String
ByteArrayOutputStream().use { outputStream ->
PrintStream(outputStream).use { printStream ->
messageCollector = GroupingMessageCollector(
PrintingMessageCollector(printStream, MessageRenderer.SYSTEM_INDEPENDENT_RELATIVE_PATHS, true),
false
)
processCompilerOutput(
messageCollector,
OutputItemsCollectorImpl(),
compilerXmlOutput,
exitCode
)
messageCollector.flush()
}
compilerOutput = outputStream.toString(Charsets.UTF_8.name())
}
return CompilerCallResult(exitCode, compilerOutput, messageCollector.hasErrors(), durationMillis)
}
private data class CompilerCallResult(
val exitCode: ExitCode,
val compilerOutput: String,
val compilerOutputHasErrors: Boolean,
val durationMillis: Long
)
private fun getLogFile(expectedArtifactFile: File): File = expectedArtifactFile.resolveSibling(expectedArtifactFile.name + ".log")
@@ -0,0 +1,212 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.konan.blackboxtest.TestCompilerArgs.Companion.EXPLICITLY_FORBIDDEN_COMPILER_ARGS
import org.jetbrains.kotlin.konan.blackboxtest.TestDirectives.ENTRY_POINT
import org.jetbrains.kotlin.konan.blackboxtest.TestDirectives.FREE_COMPILER_ARGS
import org.jetbrains.kotlin.konan.blackboxtest.TestDirectives.INPUT_DATA_FILE
import org.jetbrains.kotlin.konan.blackboxtest.TestDirectives.KIND
import org.jetbrains.kotlin.konan.blackboxtest.TestDirectives.OUTPUT_DATA_FILE
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.directives.model.StringDirective
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.test.services.impl.RegisteredDirectivesParser
import java.io.File
internal object TestDirectives : SimpleDirectivesContainer() {
val KIND by enumDirective<TestKind>(
description = """
Usage: // KIND: [REGULAR, STANDALONE, STANDALONE_NO_TR]
Declares the kind of the test:
- REGULAR (the default) - include this test into the shared test binary.
All tested functions should be annotated with @kotlin.Test.
- STANDALONE - compile the test to a separate test binary.
All tested functions should be annotated with @kotlin.Test
- STANDALONE_NO_TR - compile the test to a separate binary that is supposed to have main entry point.
The entry point can be customized Note that @kotlin.Test annotations are ignored.
""".trimIndent()
)
val ENTRY_POINT by stringDirective(
description = """
Specify custom program entry point. The default entry point is `main` function in the root package.
Note that this directive makes sense only in combination with // KIND: STANDALONE_NO_TR
""".trimIndent()
)
val MODULE by stringDirective(
"""
Usage: // MODULE: name[(dependencies)[(friends)]]
Describes one module.
""".trimIndent()
)
val FILE by stringDirective(
description = """
Usage: // FILE: name.kt
Declares file with specified name in current module.
""".trimIndent()
)
val OUTPUT_DATA_FILE by stringDirective(
description = """
Specify the file which contains the expected program output. When program finishes its execution the actual output (stdout)
will be compared to the contents of this file.
""".trimIndent()
)
val INPUT_DATA_FILE by stringDirective(
description = """
Specify the file which contains the text to be passed to process' input (stdin).
Note that this directive makes sense only in combination with // KIND: STANDALONE_NO_TR
""".trimIndent()
)
val FREE_COMPILER_ARGS by stringDirective(
description = "Specify free compiler arguments for Kotlin/Native compiler"
)
}
internal enum class TestKind {
REGULAR,
STANDALONE,
STANDALONE_NO_TR;
}
internal class TestCompilerArgs(val compilerArgs: List<String>) {
private val uniqueCompilerArgs = compilerArgs.toSet()
override fun hashCode() = uniqueCompilerArgs.hashCode()
override fun equals(other: Any?) = (other as? TestCompilerArgs)?.uniqueCompilerArgs == uniqueCompilerArgs
companion object {
val EMPTY = TestCompilerArgs(emptyList())
/** The set of compiler args that are not permitted to be explicitly specified using [FREE_COMPILER_ARGS]. */
internal val EXPLICITLY_FORBIDDEN_COMPILER_ARGS = setOf(
"-trn", "-generate-no-exit-test-runner",
"-tr", "-generate-test-runner",
"-trw", "-generate-worker-test-runner",
"-nomain",
"-output",
"-entry",
"-produce",
"-repo",
"-target",
"-Xinclude"
)
}
}
internal fun parseTestKind(registeredDirectives: RegisteredDirectives, location: Location): TestKind {
if (KIND !in registeredDirectives)
return TestKind.REGULAR // The default one.
val values = registeredDirectives[KIND]
return values.singleOrNull() ?: fail { "$location: Exactly one test kind expected in $KIND directive: $values" }
}
internal fun parseEntryPoint(registeredDirectives: RegisteredDirectives, location: Location): String {
if (ENTRY_POINT !in registeredDirectives)
return "main" // The default one.
val values = registeredDirectives[ENTRY_POINT]
val entryPoint = values.singleOrNull() ?: fail { "$location: Exactly one entry point expected in $ENTRY_POINT directive: $values" }
assertTrue(entryPoint.isNotEmpty()) { "$location: Invalid entry point in $ENTRY_POINT directive: $entryPoint" }
return entryPoint
}
internal fun parseModule(parsedDirective: RegisteredDirectivesParser.ParsedDirective, location: Location): TestModule.Exclusive {
val module = parsedDirective.values.singleOrNull()?.toString()?.let(TEST_MODULE_REGEX::matchEntire)?.let { match ->
val name = match.groupValues[1]
val directDependencySymbols = match.groupValues[3].split(',').filter(String::isNotEmpty).toSet()
val directFriendSymbols = match.groupValues[5].split(',').filter(String::isNotEmpty).toSet()
val friendsThatAreNotDependencies = directFriendSymbols - directDependencySymbols
assertTrue(friendsThatAreNotDependencies.isEmpty()) {
"$location: Found friends that are not dependencies: $friendsThatAreNotDependencies"
}
TestModule.Exclusive(name, directDependencySymbols, directFriendSymbols)
}
return module ?: fail {
"""
$location: Invalid contents of ${parsedDirective.directive} directive: ${parsedDirective.values}
${parsedDirective.directive.description}
""".trimIndent()
}
}
private val TEST_MODULE_REGEX = Regex("^([a-zA-Z0-9_]+)(\\(([a-zA-Z0-9_,]*)\\)(\\(([a-zA-Z0-9_,]*)\\))?)?$")
internal fun parseFileName(parsedDirective: RegisteredDirectivesParser.ParsedDirective, location: Location): String {
val fileName = parsedDirective.values.singleOrNull()?.toString()
?: fail {
"""
$location: Exactly one file name expected in ${parsedDirective.directive} directive: ${parsedDirective.values}
${parsedDirective.directive.description}
""".trimIndent()
}
assertTrue(fileName.endsWith(".kt") && fileName.length > 3 && '/' !in fileName && '\\' !in fileName) {
"$location: Invalid file name in ${parsedDirective.directive} directive: $fileName"
}
return fileName
}
internal fun parseFreeCompilerArgs(registeredDirectives: RegisteredDirectives, location: Location): TestCompilerArgs {
if (FREE_COMPILER_ARGS !in registeredDirectives)
return TestCompilerArgs.EMPTY
val freeCompilerArgs = registeredDirectives[FREE_COMPILER_ARGS]
val forbiddenCompilerArgs = freeCompilerArgs intersect EXPLICITLY_FORBIDDEN_COMPILER_ARGS
assertTrue(forbiddenCompilerArgs.isEmpty()) {
"""
$location: Forbidden compiler arguments found in $FREE_COMPILER_ARGS directive: $forbiddenCompilerArgs
All arguments: $freeCompilerArgs
""".trimIndent()
}
return TestCompilerArgs(freeCompilerArgs)
}
internal fun parseOutputDataFile(baseDir: File, registeredDirectives: RegisteredDirectives, location: Location): File? =
parseFileBasedDirective(baseDir, OUTPUT_DATA_FILE, registeredDirectives, location)
internal fun parseInputDataFile(baseDir: File, registeredDirectives: RegisteredDirectives, location: Location): File? =
parseFileBasedDirective(baseDir, INPUT_DATA_FILE, registeredDirectives, location)
private fun parseFileBasedDirective(
baseDir: File,
directive: StringDirective,
registeredDirectives: RegisteredDirectives,
location: Location
): File? {
if (directive !in registeredDirectives)
return null
val values = registeredDirectives[directive]
val file = values.singleOrNull()?.let { baseDir.resolve(it) }
?: fail { "$location: Exactly one file expected in $directive directive: $values" }
assertTrue(file.isFile) { "$location: File specified in $directive directive does not exist or is not a file: $file" }
return file
}
internal class Location(private val testDataFile: File, val lineNumber: Int? = null) {
override fun toString() = buildString {
append(testDataFile.path)
if (lineNumber != null) append(':').append(lineNumber + 1)
}
}
@@ -0,0 +1,117 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.konan.blackboxtest.util.TestDisposable
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.File
import java.net.URLClassLoader
internal class TestEnvironment(
val globalEnvironment: GlobalTestEnvironment,
val testRoots: TestRoots, // The directories with original sources (aka testData).
val testSourcesDir: File, // The directory with generated (preprocessed) test sources.
val sharedSourcesDir: File, // The directory with the sources of the shared modules (i.e. the modules that are widely used in multiple tests).
val testBinariesDir: File, // The directory with compiled test binaries (klibs) and executable files).
val sharedBinariesDir: File // The directory with compiled shared modules (klibs).
) : TestDisposable(parentDisposable = null)
internal class GlobalTestEnvironment(
val target: KonanTarget = HostManager.host,
val kotlinNativeHome: File = defaultKotlinNativeHome,
val lazyKotlinNativeClassLoader: Lazy<ClassLoader> = defaultKotlinNativeClassLoader,
val testMode: TestMode = defaultTestMode,
val cacheSettings: TestCacheSettings = defaultCacheSettings,
val baseBuildDir: File = projectBuildDir
) {
fun getRootCacheDirectory(debuggable: Boolean): File? =
(cacheSettings as? TestCacheSettings.WithCache)?.getRootCacheDirectory(this, debuggable)
companion object {
private val defaultKotlinNativeHome: File
get() = System.getProperty(KOTLIN_NATIVE_HOME)?.let(::File) ?: fail { "Non-specified $KOTLIN_NATIVE_HOME system property" }
// Use isolated cached class loader.
private val defaultKotlinNativeClassLoader: Lazy<ClassLoader> = lazy {
val nativeClassPath = System.getProperty(KOTLIN_NATIVE_CLASSPATH)
?.split(':', ';')
?.map { File(it).toURI().toURL() }
?.toTypedArray()
?: fail { "Non-specified $KOTLIN_NATIVE_CLASSPATH system property" }
URLClassLoader(nativeClassPath, /* no parent class loader */ null).apply { setDefaultAssertionStatus(true) }
}
private val defaultTestMode: TestMode = run {
val testModeName = System.getProperty(KOTLIN_NATIVE_TEST_MODE) ?: return@run TestMode.WITH_MODULES
TestMode.values().firstOrNull { it.name == testModeName } ?: fail {
buildString {
appendLine("Unknown test mode name $testModeName.")
appendLine("One of the following test modes should be passed through $KOTLIN_NATIVE_TEST_MODE system property:")
TestMode.values().forEach { testMode ->
appendLine("- ${testMode.name}: ${testMode.description}")
}
}
}
}
private val defaultCacheSettings: TestCacheSettings = run {
val useCacheValue = System.getProperty(KOTLIN_NATIVE_TEST_USE_CACHE)
val useCache = if (useCacheValue != null) {
useCacheValue.toBooleanStrictOrNull()
?: fail { "Invalid value for $KOTLIN_NATIVE_TEST_USE_CACHE system property: $useCacheValue" }
} else
true
if (useCache) TestCacheSettings.WithCache else TestCacheSettings.WithoutCache
}
private val projectBuildDir: File
get() = System.getenv(PROJECT_BUILD_DIR)?.let(::File) ?: fail { "Non-specified $PROJECT_BUILD_DIR environment variable" }
private const val KOTLIN_NATIVE_HOME = "kotlin.native.home"
private const val KOTLIN_NATIVE_CLASSPATH = "kotlin.internal.native.classpath"
private const val KOTLIN_NATIVE_TEST_MODE = "kotlin.internal.native.test.mode"
private const val KOTLIN_NATIVE_TEST_USE_CACHE = "kotlin.internal.native.test.useCache"
private const val PROJECT_BUILD_DIR = "PROJECT_BUILD_DIR"
}
}
internal class TestRoots(
val roots: Set<File>,
val baseDir: File
)
// TODO: in fact, only WITH_MODULES mode is supported now
internal enum class TestMode(val description: String) {
ONE_STAGE(
description = "Compile test files altogether without producing intermediate KLIBs."
),
TWO_STAGE(
description = "Compile test files altogether and produce an intermediate KLIB. Then produce a program from the KLIB using -Xinclude."
),
WITH_MODULES(
description = "Compile each test file as one or many modules (depending on MODULE directives declared in the file)." +
" Then link the KLIBs into the single executable file."
)
}
internal sealed interface TestCacheSettings {
object WithoutCache : TestCacheSettings
object WithCache : TestCacheSettings {
fun getRootCacheDirectory(globalEnvironment: GlobalTestEnvironment, debuggable: Boolean): File? = with(globalEnvironment) {
kotlinNativeHome.resolve("klib/cache").resolve(getCacheDirName(target, debuggable)).takeIf { it.exists() }
}
private const val DEFAULT_CACHE_KIND = "STATIC"
private fun getCacheDirName(target: KonanTarget, debuggable: Boolean) = "$target${if (debuggable) "-g" else ""}$DEFAULT_CACHE_KIND"
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File
internal sealed interface TestRunParameter {
fun applyTo(programArgs: MutableList<String>)
class WithPackageName(val packageName: String) : TestRunParameter {
override fun applyTo(programArgs: MutableList<String>) {
programArgs += "--ktest_filter=$packageName.*"
}
}
object WithGTestLogger : TestRunParameter {
override fun applyTo(programArgs: MutableList<String>) {
programArgs += "--ktest_logger=GTEST"
}
}
class WithInputData(val inputDataFile: File) : TestRunParameter {
override fun applyTo(programArgs: MutableList<String>) = Unit
}
class WithExpectedOutputData(val expectedOutputDataFile: File) : TestRunParameter {
override fun applyTo(programArgs: MutableList<String>) = Unit
}
}
internal class TestExecutable(
val executableFile: File,
val runParameters: List<TestRunParameter>,
val origin: TestOrigin.SingleTestDataFile,
val loggedCompilerCall: LoggedData.CompilerCall
)
internal inline fun <reified T : TestRunParameter> List<TestRunParameter>.has(): Boolean =
firstIsInstanceOrNull<T>() != null
internal inline fun <reified T : TestRunParameter> List<TestRunParameter>.get(onFound: T.() -> Unit) {
firstIsInstanceOrNull<T>()?.let(onFound)
}
@@ -0,0 +1,81 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.konan.blackboxtest.TestCompilationResult.Companion.assertSuccess
import org.jetbrains.kotlin.konan.blackboxtest.group.TestCaseGroupProvider
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.extension.ExtensionContext
import java.io.File
import java.util.concurrent.ConcurrentHashMap
internal class TestProvider(
private val environment: TestEnvironment,
private val testCaseGroupProvider: TestCaseGroupProvider
) : ExtensionContext.Store.CloseableResource {
private val compilationFactory = TestCompilationFactory(environment)
private val cachedCompilations: MutableMap<TestCompilationCacheKey, TestCompilation> = ConcurrentHashMap()
fun getTestByTestDataFile(testDataFile: File): TestExecutable {
environment.assertNotDisposed()
val testDataDir = testDataFile.parentFile
val testDataFileName = testDataFile.name
val testCaseGroup = testCaseGroupProvider.getTestCaseGroup(testDataDir) ?: fail { "No test case for $testDataFile" }
assumeTrue(testCaseGroup.isEnabled(testDataFileName), "Test case is disabled")
val testCase = testCaseGroup.getByName(testDataFileName) ?: fail { "No test case for $testDataFile" }
val testCompilation = when (testCase.kind) {
TestKind.STANDALONE, TestKind.STANDALONE_NO_TR -> {
// Create a separate compilation for each standalone test case.
cachedCompilations.computeIfAbsent(TestCompilationCacheKey.Standalone(testDataFile)) {
compilationFactory.testCasesToExecutable(listOf(testCase))
}
}
TestKind.REGULAR -> {
// Group regular test cases by compiler arguments.
cachedCompilations.computeIfAbsent(TestCompilationCacheKey.Grouped(testDataDir, testCase.freeCompilerArgs)) {
val testCases = testCaseGroup.getRegularOnlyByCompilerArgs(testCase.freeCompilerArgs)
assertTrue(testCases.isNotEmpty())
compilationFactory.testCasesToExecutable(testCases)
}
}
}
val (executableFile, loggedCompilerCall) = testCompilation.result.assertSuccess() // <-- Compilation happens here.
val runParameters = when (testCase.kind) {
TestKind.STANDALONE_NO_TR -> listOfNotNull(
testCase.extras!!.inputDataFile?.let(TestRunParameter::WithInputData),
testCase.expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
)
TestKind.STANDALONE -> listOfNotNull(
TestRunParameter.WithGTestLogger,
testCase.expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
)
TestKind.REGULAR -> listOfNotNull(
TestRunParameter.WithGTestLogger,
TestRunParameter.WithPackageName(packageName = testCase.nominalPackageName),
testCase.expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
)
}
return TestExecutable(executableFile, runParameters, testCase.origin, loggedCompilerCall)
}
override fun close() {
Disposer.dispose(environment)
}
private sealed class TestCompilationCacheKey {
data class Standalone(val testDataFile: File) : TestCompilationCacheKey()
data class Grouped(val testDataDir: File, val freeCompilerArgs: TestCompilerArgs) : TestCompilationCacheKey()
}
}
@@ -0,0 +1,125 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
import kotlin.properties.Delegates
internal fun TestExecutable.runAndVerify() {
val programArgs = mutableListOf<String>(executableFile.path)
runParameters.forEach { it.applyTo(programArgs) }
val loggedParameters = LoggedData.TestRunParameters(loggedCompilerCall, origin, programArgs, runParameters)
val startTimeMillis = System.currentTimeMillis()
val process = ProcessBuilder(programArgs).directory(executableFile.parentFile).start()
runParameters.get<TestRunParameter.WithInputData> {
process.outputStream.write(inputDataFile.readBytes())
process.outputStream.flush()
}
TestOutput(runParameters, process, startTimeMillis, loggedParameters).verify()
}
private class TestOutput(
private val runParameters: List<TestRunParameter>,
private val process: Process,
private val startTimeMillis: Long,
private val loggedParameters: LoggedData.TestRunParameters
) {
private var exitCode: Int by Delegates.notNull()
private lateinit var stdOut: String
private lateinit var stdErr: String
private var finishTimeMillis by Delegates.notNull<Long>()
fun verify() {
waitUntilExecutionFinished()
verifyExpectation(0, exitCode) { "Tested process exited with non-zero code." }
if (runParameters.has<TestRunParameter.WithGTestLogger>()) {
verifyTestWithGTestRunner()
} else {
verifyPlainTest()
}
}
private fun verifyTestWithGTestRunner() {
val testStatuses = hashMapOf<TestStatus, MutableSet<TestName>>()
val cleanStdOut = StringBuilder()
var expectStatusLine = false
stdOut.lines().forEach { line ->
when {
expectStatusLine -> {
val matcher = GTEST_STATUS_LINE_REGEX.matchEntire(line)
if (matcher != null) {
// Read the line with test status.
val testStatus = matcher.groupValues[1]
val testName = matcher.groupValues[2]
testStatuses.getOrPut(testStatus) { hashSetOf() } += testName
expectStatusLine = false
} else {
// If current line is not a status line then it could be only the line with the process' output.
cleanStdOut.appendLine(line)
}
}
line.startsWith(GTEST_RUN_LINE_PREFIX) -> {
expectStatusLine = true // Next line contains either test status.
}
else -> Unit
}
}
verifyExpectation(true, testStatuses.isNotEmpty()) { "No tests have been executed." }
val passedTests = testStatuses[GTEST_STATUS_OK]?.size ?: 0
verifyExpectation(true, passedTests > 0) { "No passed tests." }
runParameters.get<TestRunParameter.WithPackageName> {
val excessiveTests = testStatuses.getValue(GTEST_STATUS_OK).filter { testName -> !testName.startsWith(packageName) }
verifyExpectation(true, excessiveTests.isEmpty()) { "Excessive tests have been executed: $excessiveTests." }
}
val failedTests = (testStatuses - GTEST_STATUS_OK).values.sumOf { it.size }
verifyExpectation(0, failedTests) { "There are failed tests." }
runParameters.get<TestRunParameter.WithExpectedOutputData> {
val mergedOutput = cleanStdOut.toString() + stdErr
verifyExpectation(expectedOutputDataFile.readText(), mergedOutput) { "Tested process output mismatch." }
}
}
private fun verifyPlainTest() {
runParameters.get<TestRunParameter.WithExpectedOutputData> {
val mergedOutput = stdOut + stdErr
verifyExpectation(expectedOutputDataFile.readText(), mergedOutput) { "Tested process output mismatch." }
}
}
private fun waitUntilExecutionFinished() {
exitCode = process.waitFor()
finishTimeMillis = System.currentTimeMillis()
stdOut = process.inputStream.bufferedReader().readText()
stdErr = process.errorStream.bufferedReader().readText()
}
private inline fun <T> verifyExpectation(expected: T, actual: T, crossinline errorMessageHeader: () -> String) {
assertEquals(expected, actual) {
val loggedTestRun = LoggedData.TestRun(loggedParameters, exitCode, stdOut, stdErr, finishTimeMillis - startTimeMillis)
"${errorMessageHeader()}\n\n$loggedTestRun"
}
}
companion object {
private const val GTEST_RUN_LINE_PREFIX = "[ RUN ]"
private val GTEST_STATUS_LINE_REGEX = Regex("^\\[\\s+([A-Z]+)\\s+]\\s+(\\S+)\\s+.*")
private const val GTEST_STATUS_OK = "OK"
}
}
private typealias TestStatus = String
private typealias TestName = String
@@ -0,0 +1,899 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.group
import com.intellij.core.CoreApplicationEnvironment
import com.intellij.mock.MockProject
import com.intellij.openapi.Disposable
import com.intellij.pom.PomModel
import com.intellij.pom.core.impl.PomModelImpl
import com.intellij.pom.tree.TreeAspect
import com.intellij.psi.impl.source.tree.TreeCopyHandler
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.konan.blackboxtest.*
import org.jetbrains.kotlin.konan.blackboxtest.util.*
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.addRemoveModifier.addAnnotationEntry
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.resolve.ImportPath
import org.jetbrains.kotlin.resolve.checkers.OptInNames
import org.jetbrains.kotlin.test.*
import org.jetbrains.kotlin.test.InTextDirectivesUtils.isCompatibleTarget
import org.jetbrains.kotlin.test.InTextDirectivesUtils.isIgnoredTarget
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.utils.addIfNotNull
import java.io.File
internal class ExtTestCaseGroupProvider(
private val environment: TestEnvironment
) : TestCaseGroupProvider, TestDisposable(parentDisposable = environment) {
private val structureFactory = ExtTestDataFileStructureFactory(parentDisposable = this)
private val sharedModules = ThreadSafeCache<String, TestModule.Shared?>()
private val lazyTestCaseGroups = ThreadSafeFactory<File, TestCaseGroup?> { testDataDir ->
if (testDataDir in excludes) return@ThreadSafeFactory TestCaseGroup.ALL_DISABLED
val (excludedTestDataFiles, testDataFiles) = testDataDir.listFiles()
?.filter { file -> file.isFile && file.extension == "kt" }
?.partition { file -> file in excludes }
?: return@ThreadSafeFactory null
val disabledTestDataFileNames = hashSetOf<String>()
excludedTestDataFiles.mapTo(disabledTestDataFileNames) { it.name }
val testCases = mutableListOf<TestCase>()
testDataFiles.forEach { testDataFile ->
val extTestDataFile = ExtTestDataFile(environment, structureFactory, testDataFile)
if (extTestDataFile.isRelevant)
testCases += extTestDataFile.createTestCase(
definitelyStandaloneTest = testDataFile in standalones,
sharedModules = sharedModules
)
else
disabledTestDataFileNames += testDataFile.name
}
TestCaseGroup.Default(disabledTestDataFileNames, testCases)
}
override fun getTestCaseGroup(testDataDir: File): TestCaseGroup? {
assertNotDisposed()
return lazyTestCaseGroups[testDataDir]
}
companion object {
/** Test data files or test data directories that are excluded from testing. */
private val excludes: Set<File> = listOf(
"compiler/testData/codegen/boxInline/multiplatform/defaultArguments/receiversAndParametersInLambda.kt", // KT-36880
"compiler/testData/codegen/box/callableReference/genericConstructorReference.kt", // ???
"compiler/testData/codegen/box/collections/kt41123.kt", // KT-42723
"compiler/testData/codegen/box/compileKotlinAgainstKotlin/clashingFakeOverrideSignatures.kt", // KT-42020
"compiler/testData/codegen/box/compileKotlinAgainstKotlin/specialBridgesInDependencies.kt", // KT-42723
"compiler/testData/codegen/box/coroutines/multiModule/", // KT-40121
"compiler/testData/codegen/box/multiplatform/multiModule/expectActualLink.kt", // KT-41901
"compiler/testData/codegen/box/multiplatform/multiModule/expectActualMemberLink.kt", // KT-33091
"compiler/testData/codegen/box/multiplatform/multiModule/expectActualTypealiasLink.kt", // KT-40137
// Temporarily disabled because of java.lang.IllegalStateException: public final expect fun lastIndex(start: kotlin.Int, end: kotlin.Int = ...): kotlin.Unit defined in codegen.box.multiplatform.multiModule.defaultArgument.Test[SimpleFunctionDescriptorImpl@364e8b0e]
// at org.jetbrains.kotlin.backend.konan.lower.ExpectToActualDefaultValueCopier$copyDefaultArgumentsFromExpectToActual$1.visitValueParameter(ExpectDeclarationsRemoving.kt:238)
// at org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid$DefaultImpls.visitValueParameter(IrElementVisitorVoid.kt:81)
// at org.jetbrains.kotlin.backend.konan.lower.ExpectToActualDefaultValueCopier$copyDefaultArgumentsFromExpectToActual$1.visitValueParameter(ExpectDeclarationsRemoving.kt:68)
// at org.jetbrains.kotlin.backend.konan.lower.ExpectToActualDefaultValueCopier$copyDefaultArgumentsFromExpectToActual$1.visitValueParameter(ExpectDeclarationsRemoving.kt:68)
// at org.jetbrains.kotlin.ir.declarations.IrValueParameter.accept(IrValueParameter.kt:46)
// at org.jetbrains.kotlin.ir.declarations.IrFunction.acceptChildren(IrFunction.kt:56)
// at org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoidKt.acceptChildrenVoid(IrElementVisitorVoid.kt:287)
// at org.jetbrains.kotlin.backend.konan.lower.ExpectToActualDefaultValueCopier$copyDefaultArgumentsFromExpectToActual$1.visitElement(ExpectDeclarationsRemoving.kt:70)
// at org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid$DefaultImpls.visitDeclaration(IrElementVisitorVoid.kt:40)
// at org.jetbrains.kotlin.backend.konan.lower.ExpectToActualDefaultValueCopier$copyDefaultArgumentsFromExpectToActual$1.visitDeclaration(ExpectDeclarationsRemoving.kt:68)
// at org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid$DefaultImpls.visitFunction(IrElementVisitorVoid.kt:49)
// at org.jetbrains.kotlin.backend.konan.lower.ExpectToActualDefaultValueCopier$copyDefaultArgumentsFromExpectToActual$1.visitFunction(ExpectDeclarationsRemoving.kt:68)
// at org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid$DefaultImpls.visitSimpleFunction(IrElementVisitorVoid.kt:52)
// at org.jetbrains.kotlin.backend.konan.lower.ExpectToActualDefaultValueCopier$copyDefaultArgumentsFromExpectToActual$1.visitSimpleFunction(ExpectDeclarationsRemoving.kt:68)
// at org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid$DefaultImpls.visitSimpleFunction(IrElementVisitorVoid.kt:53)
// Test fails with the same exception in old test infra.
"compiler/testData/codegen/box/multiplatform/multiModule/defaultArgument.kt"
).mapToSet(::getAbsoluteFile)
/** Tests that should be compiled and executed as standalone tests. */
private val standalones: Set<File> = listOf(
"compiler/testData/codegen/box/annotations/instances/annotationToString.kt" // Comparison of type information obtained with reflection against non-patched string literal.
).mapToSet(::getAbsoluteFile)
}
}
private class ExtTestDataFile(
private val environment: TestEnvironment,
structureFactory: ExtTestDataFileStructureFactory,
private val testDataFile: File
) {
private val structure by lazy {
structureFactory.ExtTestDataFileStructure(testDataFile) { line ->
if (DIRECTIVE_REGEX.matches(line)) {
// Remove all directives from test files. These directives are not needed anymore as they are already
// read and stored in [settings] property. Moreover, these directives if left in test file can potentially conflict with
// Native-specific test directives to be added (see [TestDirectives] for details). Also, they will create unnecessary "noise".
// Examples:
// // !LANGUAGE: +NewInference
// // LANGUAGE: -ApproximateIntegerLiteralTypesInReceiverPosition
// // !USE_EXPERIMENTAL: kotlin.contracts.ExperimentalContracts
null
} else {
// Remove all diagnostic parameters from the text. Examples:
// <!NO_TAIL_CALLS_FOUND!>, <!NON_TAIL_RECURSIVE_CALL!>, <!>.
line.replace(DIAGNOSTIC_REGEX) { match -> match.groupValues[1] }
}
}
}
private val settings by lazy {
val optIns = structure.directives.multiValues(OPT_IN_DIRECTIVE)
val optInsForSourceCode = optIns subtract OPT_INS_PURELY_FOR_COMPILER
val optInsForCompiler = optIns intersect OPT_INS_PURELY_FOR_COMPILER
ExtTestDataFileSettings(
languageSettings = structure.directives.multiValues(LANGUAGE_DIRECTIVE) {
it != "+NewInference" /* It is already on by default, but passing it explicitly turns on a special "compatibility mode" in FE which is not desirable. */
},
optInsForSourceCode = optInsForSourceCode + structure.directives.multiValues(USE_EXPERIMENTAL_DIRECTIVE),
optInsForCompiler = optInsForCompiler,
expectActualLinker = EXPECT_ACTUAL_LINKER_DIRECTIVE in structure.directives,
generatedSourcesDir = computeGeneratedSourcesDir(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile,
generatedSourcesBaseDir = environment.testSourcesDir
),
effectivePackageName = computePackageName(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile
)
)
}
val isRelevant: Boolean =
isCompatibleTarget(TargetBackend.NATIVE, testDataFile) // Checks TARGET_BACKEND/DONT_TARGET_EXACT_BACKEND directives.
&& !isIgnoredTarget(TargetBackend.NATIVE, testDataFile) // Checks IGNORE_BACKEND directive.
&& settings.languageSettings.none { it in INCOMPATIBLE_LANGUAGE_SETTINGS }
&& INCOMPATIBLE_DIRECTIVES.none { it in structure.directives }
&& structure.directives[API_VERSION_DIRECTIVE] !in INCOMPATIBLE_API_VERSIONS
&& structure.directives[LANGUAGE_VERSION_DIRECTIVE] !in INCOMPATIBLE_LANGUAGE_VERSIONS
private fun assembleFreeCompilerArgs(): TestCompilerArgs {
val args = mutableListOf<String>()
settings.languageSettings.sorted().mapTo(args) { "-XXLanguage:$it" }
settings.optInsForCompiler.sorted().mapTo(args) { "-Xopt-in=$it" }
if (settings.expectActualLinker) args += "-Xexpect-actual-linker"
return TestCompilerArgs(args)
}
fun createTestCase(definitelyStandaloneTest: Boolean, sharedModules: ThreadSafeCache<String, TestModule.Shared?>): TestCase {
assertTrue(isRelevant)
val isStandaloneTest = definitelyStandaloneTest || determineIfStandaloneTest()
makeObjectsMutable()
patchPackageNames(isStandaloneTest)
val fileLevelAnnotations = patchFileLevelAnnotations()
val entryPointFunctionFQN = findEntryPoint()
generateTestLauncher(isStandaloneTest, entryPointFunctionFQN, fileLevelAnnotations)
return doCreateTestCase(isStandaloneTest, sharedModules)
}
/** Determine if the current test should be compiled as a standalone test, i.e.
* - package names are not patched
* - test is compiled independently of any other tests
*/
private fun determineIfStandaloneTest(): Boolean = with(structure) {
var isStandaloneTest = false
filesToTransform.forEach { handler ->
handler.accept(object : KtTreeVisitorVoid() {
override fun visitKtFile(file: KtFile) = when {
isStandaloneTest -> Unit
file.packageFqName.startsWith(StandardNames.BUILT_INS_PACKAGE_NAME) -> {
// We can't fully patch packages for tests containing source code in any of kotlin.* packages.
// So, such tests should be run in standalone mode to avoid possible signature clashes with other tests.
isStandaloneTest = true
}
else -> super.visitKtFile(file)
}
override fun visitCallExpression(expression: KtCallExpression) = when {
isStandaloneTest -> Unit
expression.getChildOfType<KtNameReferenceExpression>()?.getReferencedNameAsName() == TYPE_OF_NAME -> {
// Found a call of `typeOf()` function. It means that this is most likely a reflection-oriented test
// that might compare the obtained name of a type against some string literal (ex: "foo.Bar<A>"),
// which is obviously not patched during package names patching step because this step is not so smart.
// So, let's avoid patching package names for this test and let's run it in standalone mode.
isStandaloneTest = true
}
else -> super.visitCallExpression(expression)
}
})
}
isStandaloneTest
}
/** Annotate all objects and companion objects with [THREAD_LOCAL_ANNOTATION] to make them mutable. */
private fun makeObjectsMutable() = with(structure) {
filesToTransform.forEach { handler ->
handler.accept(object : KtTreeVisitorVoid() {
override fun visitObjectDeclaration(objectDeclaration: KtObjectDeclaration) {
if (!objectDeclaration.isObjectLiteral()) {
// FIXME: find only those that have vars inside
addAnnotationEntry(
objectDeclaration,
handler.psiFactory.createAnnotationEntry(THREAD_LOCAL_ANNOTATION)
).ensureSurroundedByWhiteSpace(" ")
}
super.visitObjectDeclaration(objectDeclaration)
}
})
}
}
/**
* For every Kotlin file (*.kt) stored in this text:
*
* - If there is a "package" declaration, patch it to prepend unique package prefix.
* Example: package foo -> package codegen.box.annotations.genericAnnotations.foo
*
* - If there is no "package" declaration, add one with the package name equal to unique package prefix.
* Example (new line added): package codegen.box.annotations.genericAnnotations
*
* - All "import" declarations are patched to reflect appropriate changes in "package" declarations.
* Example: import foo.* -> import codegen.box.annotations.genericAnnotations.foo.*
*
* - All fully-qualified references are patched to reflect appropriate changes in "package" declarations.
* Example: val x = foo.Bar() -> val x = codegen.box.annotations.genericAnnotations.foo.Bar()
*
* The "unique package prefix" is computed individually for every test file and reflects relative path to the test file.
* Example: codegen/box/annotations/genericAnnotations.kt -> codegen.box.annotations.genericAnnotations
*
* Note that packages with fully-qualified name starting with "kotlin." and "helpers." are kept unchanged.
* Examples: package kotlin.coroutines -> package kotlin.coroutines
* import kotlin.test.* -> import kotlin.test.*
*/
private fun patchPackageNames(isStandaloneTest: Boolean) = with(structure) {
if (isStandaloneTest) return // Don't patch packages for standalone tests.
val basePackageName = FqName(settings.effectivePackageName)
val oldPackageNames: Set<FqName> = filesToTransform.mapToSet { it.packageFqName }
val oldToNewPackageNameMapping: Map<FqName, FqName> = oldPackageNames.associateWith { oldPackageName ->
basePackageName.child(oldPackageName)
}
filesToTransform.forEach { handler ->
handler.accept(object : KtVisitor<Unit, Set<Name>>() {
override fun visitKtElement(element: KtElement, parentAccessibleDeclarationNames: Set<Name>) {
element.getChildrenOfType<KtElement>().forEach { child ->
child.accept(this, parentAccessibleDeclarationNames)
}
}
override fun visitKtFile(file: KtFile, unused: Set<Name>) {
// Patch package directive.
val oldPackageDirective = file.packageDirective
val oldPackageName = oldPackageDirective?.fqName ?: FqName.ROOT
val newPackageName = oldToNewPackageNameMapping.getValue(oldPackageName)
val newPackageDirective = handler.psiFactory.createPackageDirective(newPackageName)
if (oldPackageDirective != null) {
// Replace old package directive by the new one.
oldPackageDirective.replace(newPackageDirective).ensureSurroundedByWhiteSpace("\n\n")
} else {
// Insert the package directive immediately after file-level annotations.
file.addAfter(newPackageDirective, file.fileAnnotationList).ensureSurroundedByWhiteSpace("\n\n")
}
visitKtElement(file, file.collectAccessibleDeclarationNames())
}
override fun visitPackageDirective(directive: KtPackageDirective, unused: Set<Name>) = Unit
override fun visitImportDirective(importDirective: KtImportDirective, unused: Set<Name>) {
// Patch import directive if necessary.
val importedFqName = importDirective.importedFqName
if (importedFqName == null
|| importedFqName.startsWith(StandardNames.BUILT_INS_PACKAGE_NAME)
|| importedFqName.startsWith(HELPERS_PACKAGE_NAME)
) {
return
}
val newImportPath = ImportPath(
fqName = basePackageName.child(importedFqName),
isAllUnder = importDirective.isAllUnder,
alias = importDirective.aliasName?.let(Name::identifier)
)
importDirective.replace(handler.psiFactory.createImportDirective(newImportPath))
}
override fun visitTypeAlias(typeAlias: KtTypeAlias, parentAccessibleDeclarationNames: Set<Name>) =
super.visitTypeAlias(typeAlias, parentAccessibleDeclarationNames + typeAlias.collectAccessibleDeclarationNames())
override fun visitClassOrObject(classOrObject: KtClassOrObject, parentAccessibleDeclarationNames: Set<Name>) =
super.visitClassOrObject(
classOrObject,
parentAccessibleDeclarationNames + classOrObject.collectAccessibleDeclarationNames()
)
override fun visitClassBody(classBody: KtClassBody, parentAccessibleDeclarationNames: Set<Name>) =
super.visitClassBody(classBody, parentAccessibleDeclarationNames + classBody.collectAccessibleDeclarationNames())
override fun visitPropertyAccessor(accessor: KtPropertyAccessor, parentAccessibleDeclarationNames: Set<Name>) =
transformDeclarationWithBody(accessor, parentAccessibleDeclarationNames)
override fun visitNamedFunction(function: KtNamedFunction, parentAccessibleDeclarationNames: Set<Name>) =
transformDeclarationWithBody(function, parentAccessibleDeclarationNames)
override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor, parentAccessibleDeclarationNames: Set<Name>) =
transformDeclarationWithBody(constructor, parentAccessibleDeclarationNames)
override fun visitSecondaryConstructor(constructor: KtSecondaryConstructor, parentAccessibleDeclarationNames: Set<Name>) =
transformDeclarationWithBody(constructor, parentAccessibleDeclarationNames)
private fun transformDeclarationWithBody(
declarationWithBody: KtDeclarationWithBody,
parentAccessibleDeclarationNames: Set<Name>
) {
val (expressions, nonExpressions) = declarationWithBody.getChildrenOfType<KtElement>().partition { it is KtExpression }
val accessibleDeclarationNames =
parentAccessibleDeclarationNames + declarationWithBody.collectAccessibleDeclarationNames()
nonExpressions.forEach { it.accept(this, accessibleDeclarationNames) }
val bodyAccessibleDeclarationNames =
accessibleDeclarationNames + declarationWithBody.valueParameters.map { it.nameAsSafeName }
expressions.forEach { it.accept(this, bodyAccessibleDeclarationNames) }
}
override fun visitExpression(expression: KtExpression, parentAccessibleDeclarationNames: Set<Name>) =
if (expression is KtFunctionLiteral)
transformDeclarationWithBody(expression, parentAccessibleDeclarationNames)
else
super.visitExpression(expression, parentAccessibleDeclarationNames)
override fun visitBlockExpression(expression: KtBlockExpression, parentAccessibleDeclarationNames: Set<Name>) {
val accessibleDeclarationNames = parentAccessibleDeclarationNames.toMutableSet()
expression.getChildrenOfType<KtElement>().forEach { child ->
child.accept(this, accessibleDeclarationNames)
accessibleDeclarationNames.addIfNotNull(child.name?.let(Name::identifier))
}
}
override fun visitDotQualifiedExpression(
dotQualifiedExpression: KtDotQualifiedExpression,
accessibleDeclarationNames: Set<Name>
) {
val names = dotQualifiedExpression.collectNames()
val newDotQualifiedExpression =
visitPossiblyTypeReferenceWithFullyQualifiedName(names, accessibleDeclarationNames) { newPackageName ->
val newDotQualifiedExpression = handler.psiFactory
.createFile("val x = ${newPackageName.asString()}.${dotQualifiedExpression.text}")
.getChildOfType<KtProperty>()!!
.getChildOfType<KtDotQualifiedExpression>()!!
dotQualifiedExpression.replace(newDotQualifiedExpression) as KtDotQualifiedExpression
} ?: dotQualifiedExpression
super.visitDotQualifiedExpression(newDotQualifiedExpression, accessibleDeclarationNames)
}
override fun visitUserType(userType: KtUserType, accessibleDeclarationNames: Set<Name>) {
val names = userType.collectNames()
val newUserType =
visitPossiblyTypeReferenceWithFullyQualifiedName(names, accessibleDeclarationNames) { newPackageName ->
val newUserType = handler.psiFactory
.createFile("val x: ${newPackageName.asString()}.${userType.text}")
.getChildOfType<KtProperty>()!!
.getChildOfType<KtTypeReference>()!!
.typeElement as KtUserType
userType.replace(newUserType) as KtUserType
} ?: userType
newUserType.typeArgumentList?.let { visitKtElement(it, accessibleDeclarationNames) }
}
private fun <T : KtElement> visitPossiblyTypeReferenceWithFullyQualifiedName(
names: List<Name>,
accessibleDeclarationNames: Set<Name>,
action: (newSubPackageName: FqName) -> T
): T? {
if (names.size < 2) return null
if (names.first() in accessibleDeclarationNames) return null
for (index in 1 until names.size) {
val subPackageName = names.fqNameBeforeIndex(index)
val newPackageName = oldToNewPackageNameMapping[subPackageName]
if (newPackageName != null)
return action(newPackageName.removeSuffix(subPackageName))
}
return null
}
}, emptySet())
}
}
/**
* 1. Collect all file-level annotations that should be added to the launcher. See [generateTestLauncher].
* 2. Make sure that the OptIns specified in test directives (see [ExtTestDataFileSettings.optInsForSourceCode]) are represented
* as file-level annotations in every individual test file.
*/
private fun patchFileLevelAnnotations(): List<String> = with(structure) {
val allFileLevelAnnotations = hashSetOf<String>()
fun getAnnotationText(fullyQualifiedName: String) = "@file:${OPT_IN_ANNOTATION_NAME.asString()}($fullyQualifiedName::class)"
// Every OptIn specified in test directive should be represented as a file-level annotation.
settings.optInsForSourceCode.mapTo(allFileLevelAnnotations, ::getAnnotationText)
// Now, collect file-level annotations already present in test files.
filesToTransform.forEach { handler ->
handler.accept(object : KtTreeVisitorVoid() {
override fun visitKtFile(file: KtFile) {
val importDirectives: Map<String, String> by lazy {
file.importDirectives.mapNotNull { importDirective ->
val importedFqName = importDirective.importedFqName ?: return@mapNotNull null
val name = importDirective.alias?.name ?: importedFqName.shortName().asString()
name to importedFqName.asString()
}.toMap()
}
file.annotationEntries.mapNotNullTo(allFileLevelAnnotations) { annotationEntry ->
val constructorCallee = annotationEntry.getChildOfType<KtConstructorCalleeExpression>()
val constructorType = constructorCallee?.typeReference?.getChildOfType<KtUserType>()
val constructorTypeName = constructorType?.collectNames()?.singleOrNull()
if (constructorTypeName != OPT_IN_ANNOTATION_NAME) return@mapNotNullTo null
val valueArgument = annotationEntry.valueArguments.singleOrNull()
val classLiteral = valueArgument?.getArgumentExpression() as? KtClassLiteralExpression
if (classLiteral?.getChildOfType<KtDotQualifiedExpression>() != null)
annotationEntry.text
else
classLiteral?.getChildOfType<KtNameReferenceExpression>()?.getReferencedName()
?.let(importDirectives::get)
?.let { fullyQualifiedName -> getAnnotationText(fullyQualifiedName) }
}
}
})
}
val allFileLevelAnnotationsSorted = allFileLevelAnnotations.sorted()
// Finally, make sure that every test file contains all the necessary file-level annotations.
if (allFileLevelAnnotationsSorted.isNotEmpty()) {
filesToTransform.forEach { handler ->
handler.accept(object : KtTreeVisitorVoid() {
override fun visitKtFile(file: KtFile) {
val oldFileAnnotationList = file.fileAnnotationList
val newFileAnnotationsList = handler.psiFactory.createFile(buildString {
allFileLevelAnnotationsSorted.forEach(::appendLine)
}).fileAnnotationList!!
if (oldFileAnnotationList != null) {
// Replace old annotations list by the new one.
oldFileAnnotationList.replace(newFileAnnotationsList).ensureSurroundedByWhiteSpace("\n\n")
} else {
// Insert the annotations list immediately before package directive.
file.addBefore(newFileAnnotationsList, file.packageDirective).ensureSurroundedByWhiteSpace("\n\n")
}
}
})
}
}
return allFileLevelAnnotationsSorted
}
/** Finds the fully-qualified name of the entry point function (aka `fun box(): String`). */
private fun findEntryPoint(): String = with(structure) {
val result = mutableListOf<String>()
filesToTransform.forEach { handler ->
handler.accept(object : KtTreeVisitorVoid() {
override fun visitKtFile(file: KtFile) {
val hasBoxFunction = file.getChildrenOfType<KtNamedFunction>().any { function ->
function.name == BOX_FUNCTION_NAME.asString() && function.valueParameters.isEmpty()
}
if (hasBoxFunction) {
val boxFunctionFqName = file.packageFqName.child(BOX_FUNCTION_NAME).asString()
result += boxFunctionFqName
handler.module.markAsMain()
}
}
})
}
return result.singleOrNull()
?: fail {
"Exactly one entry point function is expected in $testDataFile. " +
"But ${if (result.size == 0) "none" else result.size} were found: $result"
}
}
/** Adds a wrapper to run it as Kotlin test. */
private fun generateTestLauncher(isStandaloneTest: Boolean, entryPointFunctionFQN: String, fileLevelAnnotations: List<String>) {
val fileText = buildString {
if (fileLevelAnnotations.isNotEmpty()) {
fileLevelAnnotations.forEach(this::appendLine)
appendLine()
}
if (!isStandaloneTest) {
append("package ").appendLine(settings.effectivePackageName)
appendLine()
}
append(
"""
@kotlin.test.Test
fun runTest() {
val result = $entryPointFunctionFQN()
kotlin.test.assertEquals("OK", result, "Test failed with: ${'$'}result")
}
""".trimIndent()
)
}
structure.addFileToMainModule(fileName = "__launcher__.kt", text = fileText)
}
private fun doCreateTestCase(
isStandaloneTest: Boolean,
sharedModules: ThreadSafeCache<String, TestModule.Shared?>
): TestCase = with(structure) {
val modules = generateModules(
testCaseDir = settings.generatedSourcesDir,
findOrGenerateSharedModule = { moduleName: String, generator: SharedModuleGenerator ->
sharedModules.computeIfAbsent(moduleName) {
generator(environment.sharedSourcesDir)
}
}
)
val testCase = TestCase(
kind = if (isStandaloneTest) TestKind.STANDALONE else TestKind.REGULAR,
modules = modules,
freeCompilerArgs = assembleFreeCompilerArgs(),
origin = TestOrigin.SingleTestDataFile(testDataFile),
nominalPackageName = settings.effectivePackageName,
expectedOutputDataFile = null,
extras = null
)
testCase.initialize(sharedModules::get)
return testCase
}
companion object {
private val INCOMPATIBLE_DIRECTIVES = setOf("FULL_JDK", "JVM_TARGET", "DIAGNOSTICS")
private const val API_VERSION_DIRECTIVE = "API_VERSION"
private val INCOMPATIBLE_API_VERSIONS = setOf("1.4")
private const val LANGUAGE_VERSION_DIRECTIVE = "LANGUAGE_VERSION"
private val INCOMPATIBLE_LANGUAGE_VERSIONS = setOf("1.3", "1.4")
private const val LANGUAGE_DIRECTIVE = "LANGUAGE"
private val INCOMPATIBLE_LANGUAGE_SETTINGS = setOf(
"-ProperIeee754Comparisons", // K/N supports only proper IEEE754 comparisons
"-ReleaseCoroutines", // only release coroutines
"-DataClassInheritance", // old behavior is not supported
"-ProhibitAssigningSingleElementsToVarargsInNamedForm", // Prohibit these assignments
"-ProhibitDataClassesOverridingCopy", // Prohibit as no longer supported
"-ProhibitOperatorMod", // Prohibit as no longer supported
"-UseBuilderInferenceOnlyIfNeeded", // Run only default one
"-UseCorrectExecutionOrderForVarargArguments" // Run only correct one
)
private const val EXPECT_ACTUAL_LINKER_DIRECTIVE = "EXPECT_ACTUAL_LINKER"
private const val USE_EXPERIMENTAL_DIRECTIVE = "USE_EXPERIMENTAL"
private const val OPT_IN_DIRECTIVE = "OPT_IN"
private val OPT_INS_PURELY_FOR_COMPILER = setOf(
OptInNames.REQUIRES_OPT_IN_FQ_NAME.asString()
)
private fun Directives.multiValues(key: String, predicate: (String) -> Boolean = { true }): Set<String> =
listValues(key)?.flatMap { it.split(' ') }?.filter(predicate)?.toSet().orEmpty()
private val DIRECTIVE_REGEX = Regex("^// !?[A-Z_]+(:?\\s+.*|\\s*)$")
private val DIAGNOSTIC_REGEX = Regex("<!.*?!>(.*?)<!>")
private const val THREAD_LOCAL_ANNOTATION = "@kotlin.native.ThreadLocal"
private val BOX_FUNCTION_NAME = Name.identifier("box")
private val OPT_IN_ANNOTATION_NAME = Name.identifier("OptIn")
private val HELPERS_PACKAGE_NAME = Name.identifier("helpers")
private val TYPE_OF_NAME = Name.identifier("typeOf")
}
}
private class ExtTestDataFileSettings(
val languageSettings: Set<String>,
val optInsForSourceCode: Set<String>,
val optInsForCompiler: Set<String>,
val expectActualLinker: Boolean,
val generatedSourcesDir: File,
val effectivePackageName: PackageName
)
private typealias SharedModuleGenerator = (sharedModulesDir: File) -> TestModule.Shared?
private typealias SharedModuleCache = (moduleName: String, generator: SharedModuleGenerator) -> TestModule.Shared?
private class ExtTestDataFileStructureFactory(parentDisposable: Disposable) : TestDisposable(parentDisposable) {
private val psiFactory = createPsiFactory(parentDisposable = this)
inner class ExtTestDataFileStructure(
originalTestDataFile: File,
initialCleanUpTransformation: (String) -> String? // Line -> transformed line (or null if the line should be omitted).
) {
init {
assertNotDisposed()
}
private val filesAndModules = FilesAndModules(originalTestDataFile, initialCleanUpTransformation)
val directives: Directives get() = filesAndModules.directives
val filesToTransform: Iterable<CurrentFileHandler>
get() = filesAndModules.parsedFiles.map { (extTestFile, psiFile) ->
object : CurrentFileHandler {
override val packageFqName get() = psiFile.packageFqName
override val module = object : CurrentFileHandler.ModuleHandler {
override fun markAsMain() {
extTestFile.module.isMain = true
}
}
override val psiFactory get() = this@ExtTestDataFileStructureFactory.psiFactory
override fun accept(visitor: KtVisitor<*, *>): Unit = psiFile.accept(visitor)
override fun <D> accept(visitor: KtVisitor<*, D>, data: D) {
psiFile.accept(visitor, data)
}
}
}
fun addFileToMainModule(fileName: String, text: String): Unit = filesAndModules.addFileToMainModule(fileName, text)
fun generateModules(testCaseDir: File, findOrGenerateSharedModule: SharedModuleCache): Set<TestModule.Exclusive> {
checkModulesConsistency()
// Generate support module, if any.
val supportModule = generateSharedSupportModule(findOrGenerateSharedModule)
// Update texts of parsed test files.
filesAndModules.parsedFiles.forEach { (extTestFile, psiFile) -> extTestFile.text = psiFile.text }
// Transform internal model into Kotlin/Native test infrastructure test model.
fun transformDependency(extTestModule: KotlinBaseTest.TestModule): String =
if (extTestModule is ExtTestModule && extTestModule.isSupport && supportModule != null) {
// Is support module is met across dependencies, then return new (unique) name for it.
supportModule.name
} else
extTestModule.name
return filesAndModules.modules.values.mapNotNullToSet { extTestModule ->
if (extTestModule.isSupport) return@mapNotNullToSet null
serializeModuleToFileSystem(
source = extTestModule,
destination = TestModule.Exclusive(
name = extTestModule.name,
directDependencySymbols = extTestModule.dependencies.mapToSet(::transformDependency),
directFriendSymbols = extTestModule.friends.mapToSet(::transformDependency)
),
baseDir = testCaseDir
) { module, file -> module.files += file }
}
}
private fun generateSharedSupportModule(findOrGenerateSharedModule: SharedModuleCache): TestModule.Shared? {
val extTestSupportModule = filesAndModules.modules[SUPPORT_MODULE_NAME] ?: return null
// Compute the module's hash. It will be used to give a unique name for the module.
val prettyHash = prettyHash(
extTestSupportModule.files.sortedBy { it.name }.fold(0) { hash, extTestFile ->
(hash * 31 + extTestFile.name.hashCode()) * 31 + extTestFile.text.hashCode()
})
val newModuleName = "${SUPPORT_MODULE_NAME}_$prettyHash"
return findOrGenerateSharedModule(newModuleName) { sharedModulesDir ->
serializeModuleToFileSystem(
source = extTestSupportModule,
destination = TestModule.Shared(newModuleName),
baseDir = sharedModulesDir
) { module, file -> module.files += file }
}
}
private fun <T : TestModule> serializeModuleToFileSystem(
source: ExtTestModule,
destination: T,
baseDir: File,
process: (T, TestFile<T>) -> Unit
): T {
val moduleDir = baseDir.resolve(destination.name)
moduleDir.mkdirs()
source.files.forEach { extTestFile ->
val file = moduleDir.resolve(extTestFile.name)
file.writeText(extTestFile.text)
process(destination, TestFile.createCommitted(file, destination))
}
return destination
}
private fun checkModulesConsistency() {
filesAndModules.modules.values.forEach { module ->
val unknownFriends = (module.friendsSymbols + module.friends.map { it.name }).toSet() - filesAndModules.modules.keys
assertTrue(unknownFriends.isEmpty()) { "Module $module has unknown friends: $unknownFriends" }
val unknownDependencies =
(module.dependenciesSymbols + module.dependencies.map { it.name }).toSet() - filesAndModules.modules.keys
assertTrue(unknownDependencies.isEmpty()) { "Module $module has unknown dependencies: $unknownDependencies" }
assertTrue(module.files.isNotEmpty()) { "Module $module has no files" }
}
}
}
private class ExtTestModule(
name: String,
dependencies: List<String>,
friends: List<String>
) : KotlinBaseTest.TestModule(name, dependencies, friends) {
val files = mutableListOf<ExtTestFile>()
val isSupport get() = name == SUPPORT_MODULE_NAME
var isMain = false
override fun equals(other: Any?) = (other as? ExtTestModule)?.name == name
override fun hashCode() = name.hashCode()
}
private class ExtTestFile(
val name: String,
val module: ExtTestModule,
var text: String
) {
init {
module.files += this
}
override fun equals(other: Any?) = (other as? ExtTestFile)?.name == name
override fun hashCode() = name.hashCode()
}
private class ExtTestFileFactory : TestFiles.TestFileFactory<ExtTestModule, ExtTestFile> {
private val defaultModule by lazy { createModule(DEFAULT_MODULE_NAME, emptyList(), emptyList(), emptyList()) }
private val supportModule by lazy { createModule(SUPPORT_MODULE_NAME, emptyList(), emptyList(), emptyList()) }
lateinit var directives: Directives
fun createFile(module: ExtTestModule, fileName: String, text: String): ExtTestFile =
ExtTestFile(getSanitizedFileName(fileName), module, text)
override fun createFile(module: ExtTestModule?, fileName: String, text: String, directives: Directives): ExtTestFile {
this.directives = directives
return createFile(
module = module ?: if (fileName == "CoroutineUtil.kt") supportModule else defaultModule,
fileName = fileName,
text = text
)
}
override fun createModule(name: String, dependencies: List<String>, friends: List<String>, abiVersions: List<Int>): ExtTestModule =
ExtTestModule(name, dependencies, friends)
}
private inner class FilesAndModules(originalTestDataFile: File, initialCleanUpTransformation: (String) -> String?) {
private val testFileFactory = ExtTestFileFactory()
private val generatedFiles = TestFiles.createTestFiles(DEFAULT_FILE_NAME, originalTestDataFile.readText(), testFileFactory)
private val lazyData: Triple<Map<String, ExtTestModule>, Map<ExtTestFile, KtFile>, MutableList<ExtTestFile>> by lazy {
// Clean up contents of every individual test file. Important: This should be done only after parsing testData file,
// because parsing of testData file relies on certain directives which could be removed by the transformation.
generatedFiles.forEach { file ->
file.text = file.text.lineSequence().mapNotNull(initialCleanUpTransformation).joinToString("\n")
}
val modules = generatedFiles.map { it.module }.associateBy { it.name }
val (supportModuleFiles, nonSupportModuleFiles) = generatedFiles.partition { it.module.isSupport }
val parsedFiles = nonSupportModuleFiles.associateWith { psiFactory.createFile(it.name, it.text) }
val nonParsedFiles = supportModuleFiles.toMutableList()
// Explicitly add support module to other modules' dependencies (as it is not listed there by default).
val supportModule = modules[SUPPORT_MODULE_NAME]
if (supportModule != null) {
modules.forEach { (moduleName, module) ->
if (moduleName != SUPPORT_MODULE_NAME && supportModule !in module.dependencies) {
module.dependencies += supportModule
}
}
}
Triple(modules, parsedFiles, nonParsedFiles)
}
val directives: Directives get() = testFileFactory.directives
val modules: Map<String, ExtTestModule> get() = lazyData.first
val parsedFiles: Map<ExtTestFile, KtFile> get() = lazyData.second
private val nonParsedFiles: MutableList<ExtTestFile> get() = lazyData.third
fun addFileToMainModule(fileName: String, text: String) {
val foundModules = modules.values.filter { it.isMain }
val mainModule = when (val size = foundModules.size) {
1 -> foundModules.first()
else -> fail { "Exactly one main module is expected. But ${if (size == 0) "none" else size} were found." }
}
nonParsedFiles += testFileFactory.createFile(mainModule, fileName, text)
}
}
interface CurrentFileHandler {
interface ModuleHandler {
fun markAsMain()
}
val packageFqName: FqName
val module: ModuleHandler
val psiFactory: KtPsiFactory
fun accept(visitor: KtVisitor<*, *>)
fun <D> accept(visitor: KtVisitor<*, D>, data: D)
}
companion object {
private fun createPsiFactory(parentDisposable: Disposable): KtPsiFactory {
val configuration: CompilerConfiguration = KotlinTestUtils.newConfiguration()
configuration.put(CommonConfigurationKeys.MODULE_NAME, "native-blackbox-test-patching-module")
val environment = KotlinCoreEnvironment.createForProduction(
parentDisposable = parentDisposable,
configuration = configuration,
configFiles = EnvironmentConfigFiles.METADATA_CONFIG_FILES
)
CoreApplicationEnvironment.registerApplicationDynamicExtensionPoint(TreeCopyHandler.EP_NAME.name, TreeCopyHandler::class.java)
val project = environment.project as MockProject
project.registerService(PomModel::class.java, PomModelImpl::class.java)
project.registerService(TreeAspect::class.java)
return KtPsiFactory(environment.project)
}
}
}
@@ -0,0 +1,256 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.group
import org.jetbrains.kotlin.konan.blackboxtest.*
import org.jetbrains.kotlin.konan.blackboxtest.util.DEFAULT_FILE_NAME
import org.jetbrains.kotlin.konan.blackboxtest.util.ThreadSafeFactory
import org.jetbrains.kotlin.konan.blackboxtest.util.computeGeneratedSourcesDir
import org.jetbrains.kotlin.konan.blackboxtest.util.computePackageName
import org.jetbrains.kotlin.test.directives.model.Directive
import org.jetbrains.kotlin.test.services.JUnit5Assertions
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertFalse
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertNotEquals
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.test.services.impl.RegisteredDirectivesParser
import java.io.File
internal interface TestCaseGroupProvider {
fun getTestCaseGroup(testDataDir: File): TestCaseGroup?
}
internal class StandardTestCaseGroupProvider(private val environment: TestEnvironment) : TestCaseGroupProvider {
// Load test cases in groups on demand.
private val lazyTestCaseGroups = ThreadSafeFactory<File, TestCaseGroup?> { testDataDir ->
val testDataFiles = testDataDir.listFiles()
?: return@ThreadSafeFactory null // `null` means that there is no such testDataDir.
val testCases = testDataFiles.mapNotNull { testDataFile ->
if (!testDataFile.isFile || testDataFile.extension != "kt")
return@mapNotNull null
createTestCase(testDataFile)
}
TestCaseGroup.Default(disabledTestDataFileNames = emptySet(), testCases = testCases)
}
override fun getTestCaseGroup(testDataDir: File) = lazyTestCaseGroups[testDataDir]
private fun createTestCase(testDataFile: File): TestCase {
val generatedSourcesDir = computeGeneratedSourcesDir(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile,
generatedSourcesBaseDir = environment.testSourcesDir
)
val effectivePackageName = computePackageName(
testDataBaseDir = environment.testRoots.baseDir,
testDataFile = testDataFile
)
val testModules = hashMapOf<String, TestModule.Exclusive>()
var currentTestModule: TestModule.Exclusive? = null
var currentTestFileName: String? = null
val currentTestFileText = StringBuilder()
val directivesParser = RegisteredDirectivesParser(TestDirectives, JUnit5Assertions)
var lastParsedDirective: Directive? = null
fun switchTestModule(newTestModule: TestModule.Exclusive, location: Location): TestModule.Exclusive {
// Don't register new test module if there is another one with the same name.
val testModule = testModules.getOrPut(newTestModule.name) { newTestModule }
assertTrue(testModule === newTestModule || testModule.haveSameSymbols(newTestModule)) {
"$location: Two declarations of the same module with different dependencies or friends found:\n$testModule\n$newTestModule"
}
currentTestModule = testModule
return testModule
}
fun beginTestFile(fileName: String) {
assertEquals(null, currentTestFileName)
currentTestFileName = fileName
}
fun finishTestFile(forceFinish: Boolean, location: Location) {
val needToFinish = forceFinish
|| currentTestFileName != null
|| (currentTestFileName == null /*&& testFiles.isEmpty()*/ && currentTestFileText.hasAnythingButComments())
if (needToFinish) {
val fileName = currentTestFileName ?: DEFAULT_FILE_NAME
val testModule = currentTestModule ?: switchTestModule(TestModule.newDefaultModule(), location)
testModule.files += TestFile.createUncommitted(
location = generatedSourcesDir.resolve(fileName),
module = testModule,
text = currentTestFileText
)
currentTestFileText.clear()
repeat(location.lineNumber ?: 0) { currentTestFileText.appendLine() }
currentTestFileName = null
}
}
testDataFile.readLines().forEachIndexed { lineNumber, line ->
val location = Location(testDataFile, lineNumber)
val expectFileDirectiveAfterModuleDirective =
lastParsedDirective == TestDirectives.MODULE // Only FILE directive may follow MODULE directive.
val rawDirective = RegisteredDirectivesParser.parseDirective(line)
if (rawDirective != null) {
val parsedDirective = try {
directivesParser.convertToRegisteredDirective(rawDirective)
} catch (e: AssertionError) {
// Enhance error message with concrete test data file and line number where the error has happened.
throw AssertionError("$location: Error while parsing directive in test data file.\nCause: ${e.message}", e)
}
if (parsedDirective != null) {
when (val directive = parsedDirective.directive) {
TestDirectives.FILE -> {
val newFileName = parseFileName(parsedDirective, location)
finishTestFile(forceFinish = false, location)
beginTestFile(newFileName)
}
else -> {
assertFalse(expectFileDirectiveAfterModuleDirective) {
"$location: Directive $directive encountered after ${TestDirectives.MODULE} directive but was expecting ${TestDirectives.FILE}"
}
when (directive) {
TestDirectives.MODULE -> {
finishTestFile(forceFinish = false, location)
switchTestModule(parseModule(parsedDirective, location), location)
}
else -> {
assertNotEquals(TestDirectives.FILE, lastParsedDirective) {
"$location: Global directive $directive encountered after ${TestDirectives.FILE} directive"
}
assertNotEquals(TestDirectives.MODULE, lastParsedDirective) {
"$location: Global directive $directive encountered after ${TestDirectives.MODULE} directive"
}
directivesParser.addParsedDirective(parsedDirective)
}
}
}
}
currentTestFileText.appendLine()
lastParsedDirective = parsedDirective.directive
return@forEachIndexed
}
}
if (expectFileDirectiveAfterModuleDirective) {
// Was expecting a line with the FILE directive as this is the only possible continuation of a line with the MODULE directive, but failed.
fail { "$location: ${TestDirectives.FILE} directive expected after ${TestDirectives.MODULE} directive" }
}
currentTestFileText.appendLine(line)
}
val location = Location(testDataFile)
finishTestFile(forceFinish = true, location)
val registeredDirectives = directivesParser.build()
val freeCompilerArgs = parseFreeCompilerArgs(registeredDirectives, location)
val expectedOutputDataFile = parseOutputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
val testKind = parseTestKind(registeredDirectives, location)
if (testKind == TestKind.REGULAR) {
// Fix package declarations to avoid unintended conflicts between symbols with the same name in different test cases.
testModules.values.forEach { testModule ->
testModule.files.forEach { testFile -> fixPackageDeclaration(testFile, effectivePackageName, testDataFile) }
}
}
val testCase = TestCase(
kind = testKind,
modules = testModules.values.toSet(),
freeCompilerArgs = freeCompilerArgs,
origin = TestOrigin.SingleTestDataFile(testDataFile),
nominalPackageName = effectivePackageName,
expectedOutputDataFile = expectedOutputDataFile,
extras = if (testKind == TestKind.STANDALONE_NO_TR) {
TestCase.StandaloneNoTestRunnerExtras(
entryPoint = parseEntryPoint(registeredDirectives, location),
inputDataFile = parseInputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
)
} else
null
)
testCase.initialize(findSharedModule = null)
return testCase
}
private fun CharSequence.hasAnythingButComments(): Boolean {
var result = false
runForFirstMeaningfulStatement { _, _ -> result = true }
return result
}
private fun fixPackageDeclaration(
testFile: TestFile<TestModule.Exclusive>,
packageName: PackageName,
testDataFile: File
) = testFile.update { text ->
var existingPackageDeclarationLine: String? = null
var existingPackageDeclarationLineNumber: Int? = null
text.runForFirstMeaningfulStatement { lineNumber, line ->
// First meaningful line.
val trimmedLine = line.trim()
if (trimmedLine.startsWith("package ")) {
existingPackageDeclarationLine = trimmedLine
existingPackageDeclarationLineNumber = lineNumber
}
}
if (existingPackageDeclarationLine != null) {
val existingPackageName = existingPackageDeclarationLine!!.substringAfter("package ").trimStart()
assertTrue(
existingPackageName == packageName
|| (existingPackageName.length > packageName.length
&& existingPackageName.startsWith(packageName)
&& existingPackageName[packageName.length] == '.')
) {
val location = Location(testDataFile, existingPackageDeclarationLineNumber)
"$location: Invalid package name declaration found: $existingPackageDeclarationLine\nExpected: package $packageName"
}
text
} else
"package $packageName $text"
}
private inline fun CharSequence.runForFirstMeaningfulStatement(action: (lineNumber: Int, line: String) -> Unit) {
var inMultilineComment = false
for ((lineNumber, line) in lines().withIndex()) {
val trimmedLine = line.trim()
when {
inMultilineComment -> inMultilineComment = !trimmedLine.endsWith("*/")
trimmedLine.startsWith("/*") -> inMultilineComment = true
trimmedLine.isEmpty() -> Unit
trimmedLine.startsWith("//") -> Unit
trimmedLine.startsWith("@file:") -> Unit
else -> {
action(lineNumber, line)
break
}
}
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import java.util.*
internal fun <T> Collection<T>.toIdentitySet(): Set<T> =
Collections.newSetFromMap(IdentityHashMap<T, Boolean>()).apply { addAll(this@toIdentitySet) }
internal class FailOnDuplicatesSet<E : Any> : Set<E> {
private val uniqueElements: MutableSet<E> = hashSetOf()
operator fun plusAssign(element: E) {
assertTrue(uniqueElements.add(element)) { "An attempt to add already existing element: $element" }
}
override val size get() = uniqueElements.size
override fun isEmpty() = uniqueElements.isEmpty()
override fun contains(element: E) = element in uniqueElements
override fun containsAll(elements: Collection<E>) = uniqueElements.containsAll(elements)
override fun iterator(): Iterator<E> = uniqueElements.iterator()
override fun equals(other: Any?) = (other as? FailOnDuplicatesSet<*>)?.uniqueElements == uniqueElements
override fun hashCode() = uniqueElements.hashCode()
}
internal inline fun <T, R> Iterable<T>.mapToSet(transform: (T) -> R): Set<R> {
if (this is Collection && isEmpty()) return emptySet()
val result = hashSetOf<R>()
mapTo(result, transform)
return result
}
internal inline fun <T, R : Any> Iterable<T>.mapNotNullToSet(transform: (T) -> R?): Set<R> {
if (this is Collection && isEmpty()) return emptySet()
val result = hashSetOf<R>()
mapNotNullTo(result, transform)
return result
}
internal inline fun <T, R : Any> Array<out T>.mapNotNullToSet(transform: (T) -> R?): Set<R> {
if (isEmpty()) return emptySet()
val result = hashSetOf<R>()
mapNotNullTo(result, transform)
return result
}
internal inline fun <T, R> Iterable<T>.flatMapToSet(transform: (T) -> Iterable<R>): Set<R> {
if (this is Collection && isEmpty()) return emptySet()
val result = hashSetOf<R>()
flatMapTo(result, transform)
return result
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import org.jetbrains.kotlin.storage.NotNullLazyValue
import org.jetbrains.kotlin.storage.StorageManager
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
internal fun <N : Any> StorageManager.lazyNeighbors(
directNeighbors: () -> Set<N>,
computeNeighbors: (N) -> Set<N>,
describe: (N) -> String = { it.toString() }
): ReadOnlyProperty<N, Set<N>> = object : ReadOnlyProperty<N, Set<N>> {
private val lazyValue: NotNullLazyValue<Set<N>> = this@lazyNeighbors.createLazyValue(
computable = {
val neighbors = directNeighbors()
if (neighbors.isEmpty())
emptySet()
else
hashSetOf<N>().apply {
addAll(neighbors)
neighbors.forEach { neighbor -> addAll(computeNeighbors(neighbor)) }
}
},
onRecursiveCall = { throw CyclicNeighborsException() })
override fun getValue(thisRef: N, property: KProperty<*>): Set<N> = try {
lazyValue.invoke()
} catch (e: CyclicNeighborsException) {
throw e.trace("Property ${property.name} of ${describe(thisRef)}")
}
}
private class CyclicNeighborsException : Exception() {
private val backtrace = mutableListOf<String>()
fun trace(element: String) = apply { backtrace += element }
override val message
get() = buildString {
appendLine("Cyclic neighbors detected. Backtrace (${backtrace.size} elements):")
backtrace.joinTo(this, separator = "\n")
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertFalse
internal open class TestDisposable(parentDisposable: Disposable?) : Disposable {
init {
if (parentDisposable != null) {
@Suppress("LeakingThis")
Disposer.register(parentDisposable, this)
}
}
@Volatile
private var disposed = false
final override fun dispose() {
disposed = true
}
fun assertNotDisposed() {
assertFalse(disposed) { "Already disposed: ${this::class.java}, $this" }
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.util.KtTestUtil.getHomeDirectory
import java.io.File
internal fun File.ensureExistsAndIsEmptyDirectory(): File {
if (exists()) deleteRecursively()
mkdirs()
return this
}
internal fun getAbsoluteFile(localPath: String): File = File(getHomeDirectory()).resolve(localPath).canonicalFile
internal fun computeGeneratedSourcesDir(testDataBaseDir: File, testDataFile: File, generatedSourcesBaseDir: File): File {
assertTrue(testDataFile.startsWith(testDataBaseDir)) {
"The file is outside of the directory.\nFile: $testDataFile\nDirectory: $testDataBaseDir"
}
val testDataFileDir = testDataFile.parentFile
return generatedSourcesBaseDir
.resolve(testDataFileDir.relativeTo(testDataBaseDir))
.resolve(testDataFile.nameWithoutExtension)
}
@@ -0,0 +1,19 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
internal val Class<*>.sanitizedName: String
get() = name.map { if (it.isLetterOrDigit() || it == '_') it else '_' }.joinToString("")
internal fun getSanitizedFileName(fileName: String): String =
fileName.map { if (it.isLetterOrDigit() || it == '_' || it == '.') it else '_' }.joinToString("")
internal const val DEFAULT_FILE_NAME = "main.kt"
internal const val DEFAULT_MODULE_NAME = "default"
internal const val SUPPORT_MODULE_NAME = "support"
internal fun prettyHash(hash: Int): String = hash.toUInt().toString(16).padStart(8, '0')
@@ -0,0 +1,59 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import org.jetbrains.kotlin.konan.blackboxtest.PackageName
import org.jetbrains.kotlin.renderer.KeywordStringsGenerated.KEYWORDS
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import java.io.File
import java.nio.file.Path
import kotlin.io.path.name
import kotlin.math.min
internal fun computePackageName(testDataBaseDir: File, testDataFile: File): PackageName {
assertTrue(testDataFile.startsWith(testDataBaseDir)) {
"The file is outside of the directory.\nFile: $testDataFile\nDirectory: $testDataBaseDir"
}
return testDataFile.parentFile
.relativeTo(testDataBaseDir)
.resolve(testDataFile.nameWithoutExtension)
.toPath()
.map(Path::name)
.joinToString(".") { packagePart ->
if (packagePart in KEYWORDS)
"_${packagePart}_"
else
buildString {
packagePart.forEachIndexed { index, ch ->
if (index == 0) when {
ch.isJavaIdentifierStart() -> append(ch)
ch.isJavaIdentifierPart() -> append('_').append(ch)
else -> append('_')
}
else append(if (ch.isJavaIdentifierPart()) ch else '_')
}
}
}
}
internal fun Set<PackageName>.findCommonPackageName(): PackageName? = when (size) {
0 -> null
1 -> first()
else -> map { packageName: PackageName ->
packageName.split('.')
}.reduce { commonPackageNameParts: List<String>, packageNameParts: List<String> ->
ArrayList<String>(min(commonPackageNameParts.size, packageNameParts.size)).apply {
val i = commonPackageNameParts.iterator()
val j = packageNameParts.iterator()
while (i.hasNext() && j.hasNext()) {
val packageNamePart = i.next()
if (packageNamePart == j.next()) add(packageNamePart) else break
}
}
}.takeIf { it.isNotEmpty() }?.joinToString(".")
}
@@ -0,0 +1,109 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
import org.jetbrains.kotlin.utils.addIfNotNull
internal fun FqName.child(child: FqName): FqName =
child.pathSegments().fold(this) { accumulator, segment -> accumulator.child(segment) }
internal fun List<Name>.fqNameBeforeIndex(toIndexExclusive: Int): FqName =
if (toIndexExclusive == 0) FqName.ROOT else FqName(subList(0, toIndexExclusive).joinToString("."))
internal fun FqName.removeSuffix(suffix: FqName): FqName {
val pathSegments = pathSegments()
val suffixPathSegments = suffix.pathSegments()
val suffixStart = pathSegments.size - suffixPathSegments.size
assertEquals(suffixPathSegments, pathSegments.subList(suffixStart, pathSegments.size))
return FqName(pathSegments.take(suffixStart).joinToString("."))
}
internal fun KtDotQualifiedExpression.collectNames(): List<Name> {
val output = mutableListOf<Name>()
fun KtExpression.recurse(): Boolean {
children.forEach { child ->
when (child) {
is KtExpression -> when (child) {
is KtDotQualifiedExpression -> if (!child.recurse()) return false
is KtCallExpression,
is KtArrayAccessExpression,
is KtClassLiteralExpression,
is KtPostfixExpression -> {
child.recurse()
return false
}
is KtCallableReferenceExpression -> {
// 'T' from 'T::foo' should be considered, '::foo' should be discarded.
child.getChildrenOfType<KtNameReferenceExpression>()
.takeIf { it.size == 2 }
?.first()
?.let { output += it.getReferencedNameAsName() }
return false
}
is KtSafeQualifiedExpression -> {
// Consider only the first KtNameReferenceExpression child.
output.addIfNotNull(child.getChildOfType<KtNameReferenceExpression>()?.getReferencedNameAsName())
return false
}
is KtNameReferenceExpression -> output += child.getReferencedNameAsName()
else -> return false
}
else -> return false
}
}
return true
}
recurse()
return output
}
internal fun KtUserType.collectNames(output: MutableList<Name> = mutableListOf()): List<Name> {
children.forEach { child ->
when (child) {
is KtUserType -> child.collectNames(output)
is KtNameReferenceExpression -> output += child.getReferencedNameAsName()
else -> Unit
}
}
return output
}
internal fun KtElement.collectAccessibleDeclarationNames(): Set<Name> {
val names = hashSetOf<Name>()
if (this is KtTypeParameterListOwner) {
typeParameters.mapTo(names) { it.nameAsSafeName }
}
children.forEach { child ->
if (child is KtNamedDeclaration) {
when (child) {
is KtClassLikeDeclaration,
is KtVariableDeclaration,
is KtParameter,
is KtTypeParameter -> names += child.nameAsSafeName
}
}
if (child is KtDestructuringDeclaration) {
child.entries.mapTo(names) { it.nameAsSafeName }
}
}
return names
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.psiUtil.nextLeaf
import org.jetbrains.kotlin.psi.psiUtil.prevLeaf
internal fun PsiElement.ensureSurroundedByWhiteSpace(whiteSpace: String): PsiElement =
ensureHasWhiteSpaceBefore(whiteSpace).ensureHasWhiteSpaceAfter(whiteSpace)
private fun PsiElement.ensureHasWhiteSpaceBefore(whiteSpace: String): PsiElement {
val (fileBoundaryReached, whiteSpaceBefore) = whiteSpaceBefore()
if (!fileBoundaryReached and !whiteSpaceBefore.endsWith(whiteSpace)) {
parent.addBefore(KtPsiFactory(project).createWhiteSpace(whiteSpace), this)
}
return this
}
private fun PsiElement.ensureHasWhiteSpaceAfter(whiteSpace: String): PsiElement {
val (fileBoundaryReached, whiteSpaceAfter) = whiteSpaceAfter()
if (!fileBoundaryReached and !whiteSpaceAfter.startsWith(whiteSpace)) {
parent.addAfter(KtPsiFactory(project).createWhiteSpace(whiteSpace), this)
}
return this
}
private fun PsiElement.whiteSpaceBefore(): Pair<Boolean, String> {
var fileBoundaryReached = false
fun PsiElement.prevWhiteSpace(): PsiWhiteSpace? = when (val prevLeaf = prevLeaf(skipEmptyElements = true)) {
null -> {
fileBoundaryReached = true
null
}
else -> prevLeaf as? PsiWhiteSpace
}
val whiteSpace = buildString {
generateSequence(prevWhiteSpace()) { it.prevWhiteSpace() }.toList().asReversed().forEach { append(it.text) }
}
return fileBoundaryReached to whiteSpace
}
private fun PsiElement.whiteSpaceAfter(): Pair<Boolean, String> {
var fileBoundaryReached = false
fun PsiElement.nextWhiteSpace(): PsiWhiteSpace? = when (val nextLeaf = nextLeaf(skipEmptyElements = true)) {
null -> {
fileBoundaryReached = true
null
}
else -> nextLeaf as? PsiWhiteSpace
}
val whiteSpace = buildString {
generateSequence(nextWhiteSpace()) { it.nextWhiteSpace() }.forEach { append(it.text) }
}
return fileBoundaryReached to whiteSpace
}
@@ -0,0 +1,38 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.konan.blackboxtest.util
import org.jetbrains.kotlin.konan.blackboxtest.util.NullStorage.unwrap
import org.jetbrains.kotlin.konan.blackboxtest.util.NullStorage.wrap
import java.util.concurrent.ConcurrentHashMap
/**
* Permits null values. Calls [function] at most once per every key in concurrent environment.
*/
@Suppress("UNCHECKED_CAST")
internal class ThreadSafeFactory<K : Any, V>(private val function: (K) -> V) {
private val map = ConcurrentHashMap<K, Any>()
operator fun get(key: K): V = unwrap(map.computeIfAbsent(key) { wrap(function(key)) }) as V
}
/**
* Permits null values. Atomic modifications in concurrent environment.
*/
@Suppress("UNCHECKED_CAST")
internal class ThreadSafeCache<K : Any, V> {
private val map = ConcurrentHashMap<K, Any>()
operator fun get(key: K): V = unwrap(map[key]) as V
fun computeIfAbsent(key: K, function: (K) -> V): V = unwrap(map.computeIfAbsent(key) { wrap(function(key)) }) as V
}
private object NullStorage {
private val NULL_OBJECT = Any()
fun wrap(value: Any?): Any = value ?: NULL_OBJECT
fun unwrap(value: Any?): Any? = if (value == NULL_OBJECT) null else value
}
+2
View File
@@ -172,6 +172,7 @@ include ":benchmarks",
":native:kotlin-klib-commonizer",
":native:kotlin-klib-commonizer-api",
":native:kotlin-klib-commonizer-embeddable",
":native:tests-blackbox.native",
":core:compiler.common",
":core:compiler.common.jvm",
":compiler:backend.common.jvm",
@@ -589,6 +590,7 @@ project(':kotlin-util-klib').projectDir = "$rootDir/compiler/util-klib" as File
project(':kotlin-util-klib-metadata').projectDir = "$rootDir/compiler/util-klib-metadata" as File
project(':native:kotlin-native-utils').projectDir = "$rootDir/native/utils" as File
project(':native:frontend.native').projectDir = "$rootDir/native/frontend" as File
project(':native:tests-blackbox.native').projectDir = "$rootDir/native/tests-blackbox" as File
project(':native:kotlin-klib-commonizer').projectDir = "$rootDir/native/commonizer" as File
project(":native:kotlin-klib-commonizer-api").projectDir = "$rootDir/native/commonizer-api" as File
project(':native:kotlin-klib-commonizer-embeddable').projectDir = "$rootDir/native/commonizer-embeddable" as File