[Native] New test infrastructure for black box tests
This commit is contained in:
@@ -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)
|
||||
}
|
||||
+51273
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
42
|
||||
42
|
||||
42
|
||||
42
|
||||
@@ -0,0 +1 @@
|
||||
Hello, Kotlin!
|
||||
@@ -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)
|
||||
}
|
||||
+39
@@ -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)
|
||||
}
|
||||
+51
@@ -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)
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
+40
@@ -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)
|
||||
}
|
||||
+39
@@ -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)
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
+21
@@ -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")
|
||||
}
|
||||
+15
@@ -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")
|
||||
}
|
||||
+7
@@ -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)
|
||||
}
|
||||
+193
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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<*>)
|
||||
+32
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -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")
|
||||
}
|
||||
}
|
||||
+131
@@ -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")
|
||||
+212
@@ -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)
|
||||
}
|
||||
}
|
||||
+117
@@ -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
|
||||
+899
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+256
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -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
|
||||
}
|
||||
+48
@@ -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")
|
||||
}
|
||||
}
|
||||
+30
@@ -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')
|
||||
+59
@@ -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
|
||||
}
|
||||
+67
@@ -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
|
||||
}
|
||||
+38
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user