[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",
|
||||||
":native:kotlin-klib-commonizer-api",
|
":native:kotlin-klib-commonizer-api",
|
||||||
":native:kotlin-klib-commonizer-embeddable",
|
":native:kotlin-klib-commonizer-embeddable",
|
||||||
|
":native:tests-blackbox.native",
|
||||||
":core:compiler.common",
|
":core:compiler.common",
|
||||||
":core:compiler.common.jvm",
|
":core:compiler.common.jvm",
|
||||||
":compiler:backend.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(':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:kotlin-native-utils').projectDir = "$rootDir/native/utils" as File
|
||||||
project(':native:frontend.native').projectDir = "$rootDir/native/frontend" 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').projectDir = "$rootDir/native/commonizer" as File
|
||||||
project(":native:kotlin-klib-commonizer-api").projectDir = "$rootDir/native/commonizer-api" 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
|
project(':native:kotlin-klib-commonizer-embeddable').projectDir = "$rootDir/native/commonizer-embeddable" as File
|
||||||
|
|||||||
Reference in New Issue
Block a user