diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index eb353af0..677bd527 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -1,4 +1,3 @@ - buildscript { /* @@ -59,8 +58,11 @@ kotlin { } dependencies { - testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testImplementation gradleTestKit() + api "org.ow2.asm:asm:$asm_version" + api "org.ow2.asm:asm-commons:$asm_version" } sourceSets { @@ -80,4 +82,6 @@ task mavenTest(type: Test) { def sourceSet = sourceSets.mavenTest testClassesDirs = sourceSet.output.classesDirs classpath = sourceSet.runtimeClasspath -} \ No newline at end of file +} + +// todo: set atomicfu system property \ No newline at end of file diff --git a/integration-testing/examples/jvm-sample/build.gradle.kts b/integration-testing/examples/jvm-sample/build.gradle.kts new file mode 100644 index 00000000..81020d8b --- /dev/null +++ b/integration-testing/examples/jvm-sample/build.gradle.kts @@ -0,0 +1,28 @@ +buildscript { + val atomicfu_version = rootProject.properties["atomicfu_version"] + + repositories { + mavenLocal() + } + + dependencies { + classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version") + } +} + +plugins { + kotlin("jvm") version "1.9.10" // todo get kotlin_version from gradle.properties +} + +apply(plugin = "kotlinx-atomicfu") + +repositories { + mavenLocal() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") +} + +dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-junit")) +} diff --git a/integration-testing/examples/jvm-sample/gradle.properties b/integration-testing/examples/jvm-sample/gradle.properties new file mode 100644 index 00000000..07723301 --- /dev/null +++ b/integration-testing/examples/jvm-sample/gradle.properties @@ -0,0 +1,2 @@ +kotlin_version=1.9.0 +atomicfu_version=0.22.0-SNAPSHOT \ No newline at end of file diff --git a/integration-testing/examples/jvm-sample/settings.gradle.kts b/integration-testing/examples/jvm-sample/settings.gradle.kts new file mode 100644 index 00000000..98b5f332 --- /dev/null +++ b/integration-testing/examples/jvm-sample/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "jvm-sample" \ No newline at end of file diff --git a/integration-testing/examples/jvm-sample/src/main/kotlin/Sample.kt b/integration-testing/examples/jvm-sample/src/main/kotlin/Sample.kt new file mode 100644 index 00000000..457fa501 --- /dev/null +++ b/integration-testing/examples/jvm-sample/src/main/kotlin/Sample.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.atomicfu.* +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class IntArithmetic { + private val _x = atomic(0) + val x get() = _x.value + + fun doWork(finalValue: Int) { + assertEquals(0, x) + assertEquals(0, _x.getAndSet(3)) + assertEquals(3, x) + assertTrue(_x.compareAndSet(3, finalValue)) + } +} \ No newline at end of file diff --git a/integration-testing/examples/jvm-sample/src/test/kotlin/SampleTest.kt b/integration-testing/examples/jvm-sample/src/test/kotlin/SampleTest.kt new file mode 100644 index 00000000..04f22569 --- /dev/null +++ b/integration-testing/examples/jvm-sample/src/test/kotlin/SampleTest.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlin.test.* + +class ArithmeticTest { + @Test + fun testInt() { + val a = IntArithmetic() + a.doWork(1234) + assertEquals(1234, a.x) + } +} \ No newline at end of file diff --git a/integration-testing/examples/mpp-sample/build.gradle.kts b/integration-testing/examples/mpp-sample/build.gradle.kts new file mode 100644 index 00000000..26f82279 --- /dev/null +++ b/integration-testing/examples/mpp-sample/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +buildscript { + val atomicfu_version = rootProject.properties["atomicfu_version"] + + repositories { + mavenLocal() + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version") + } +} + +plugins { + kotlin("multiplatform") version "1.9.10" // todo get kotlin_version from gradle.proeprties +} + +apply(plugin = "kotlinx-atomicfu") + +repositories { + mavenLocal() + mavenCentral() +} + +kotlin { + jvm() + macosX64("native") + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} \ No newline at end of file diff --git a/integration-testing/examples/mpp-sample/gradle.properties b/integration-testing/examples/mpp-sample/gradle.properties new file mode 100644 index 00000000..07723301 --- /dev/null +++ b/integration-testing/examples/mpp-sample/gradle.properties @@ -0,0 +1,2 @@ +kotlin_version=1.9.0 +atomicfu_version=0.22.0-SNAPSHOT \ No newline at end of file diff --git a/integration-testing/examples/mpp-sample/settings.gradle.kts b/integration-testing/examples/mpp-sample/settings.gradle.kts new file mode 100644 index 00000000..d8758310 --- /dev/null +++ b/integration-testing/examples/mpp-sample/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "mpp-sample" \ No newline at end of file diff --git a/integration-testing/examples/mpp-sample/src/commonMain/kotlin/IntArithmetic.kt b/integration-testing/examples/mpp-sample/src/commonMain/kotlin/IntArithmetic.kt new file mode 100644 index 00000000..a1e02b3b --- /dev/null +++ b/integration-testing/examples/mpp-sample/src/commonMain/kotlin/IntArithmetic.kt @@ -0,0 +1,14 @@ +import kotlinx.atomicfu.* + +class IntArithmetic { + private val _x = atomic(0) + val x get() = _x.value + + fun doWork(finalValue: Int) { + check(x == 0) + _x.getAndSet(3) + check(x == 3) + _x.compareAndSet(3, finalValue) + check(x == finalValue) + } +} \ No newline at end of file diff --git a/integration-testing/examples/mpp-sample/src/commonTest/kotlin/IntArithmeticTest.kt b/integration-testing/examples/mpp-sample/src/commonTest/kotlin/IntArithmeticTest.kt new file mode 100644 index 00000000..7ac25bf7 --- /dev/null +++ b/integration-testing/examples/mpp-sample/src/commonTest/kotlin/IntArithmeticTest.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlin.test.* + +class IntArithmeticTest { + + @Test + fun testInt() { + val a = IntArithmetic() + a.doWork(1234) + assertEquals(1234, a.x) + } +} \ No newline at end of file diff --git a/integration-testing/gradle.properties b/integration-testing/gradle.properties index fe9caf7f..76e6a4b0 100644 --- a/integration-testing/gradle.properties +++ b/integration-testing/gradle.properties @@ -1,5 +1,5 @@ kotlin_version=1.9.0 atomicfu_version=0.22.0-SNAPSHOT - +asm_version=9.3 kotlin.code.style=official kotlin.mpp.stability.nowarn=true diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/JvmProjectTest.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/JvmProjectTest.kt new file mode 100644 index 00000000..8ea67087 --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/JvmProjectTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.gradle.plugin.test.cases + +import kotlinx.atomicfu.gradle.plugin.test.framework.checker.* +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.* +import kotlin.test.Test + +class JvmProjectTest { + + private val jvmSample: GradleBuild = createGradleBuildFromSources("jvm-sample") + + @Test + fun testJvmWithEnabledIrTransformation() { + jvmSample.enableJvmIrTransformation = true + jvmSample.checkJvmCompileOnlyDependencies() + jvmSample.buildAndCheckBytecode() + } + + @Test + fun testJvmWithDisabledIrTransformation() { + jvmSample.enableJvmIrTransformation = false + jvmSample.checkJvmCompileOnlyDependencies() + jvmSample.buildAndCheckBytecode() + } +} \ No newline at end of file diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt new file mode 100644 index 00000000..e7de620c --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package test + +import kotlinx.atomicfu.gradle.plugin.test.framework.checker.* +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.* +import kotlin.test.Test + +class MppProjectTest { + private val mppSample: GradleBuild = createGradleBuildFromSources("mpp-sample") + + @Test + fun testMppJvm1() { + mppSample.enableJvmIrTransformation = true + mppSample.checkMppJvmCompileOnlyDependencies() + mppSample.buildAndCheckBytecode() + } + + @Test + fun testMppJvm2() { + mppSample.enableJvmIrTransformation = false + mppSample.checkMppJvmCompileOnlyDependencies() + mppSample.buildAndCheckBytecode() + } +} \ No newline at end of file diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt new file mode 100644 index 00000000..c604c68c --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.gradle.plugin.test.framework.checker + +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.GradleBuild +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.build +import org.objectweb.asm.* +import java.io.File +import kotlin.test.assertFalse + +internal abstract class ArtifactChecker(val targetDir: File) { + + private val ATOMIC_FU_REF = "Lkotlinx/atomicfu/".toByteArray() + protected val KOTLIN_METADATA_DESC = "Lkotlin/Metadata;" + + protected val projectName = targetDir.name.substringBeforeLast("-") + + val buildDir + get() = targetDir.resolve("build").also { + require(it.exists() && it.isDirectory) { "Could not find `build/` directory in the target directory of the project $projectName: ${targetDir.path}" } + } + + abstract fun checkReferences() + + protected fun ByteArray.findAtomicfuRef(): Boolean { + loop@for (i in 0 until this.size - ATOMIC_FU_REF.size) { + for (j in ATOMIC_FU_REF.indices) { + if (this[i + j] != ATOMIC_FU_REF[j]) continue@loop + } + return true + } + return false + } +} + +private class BytecodeChecker(targetDir: File) : ArtifactChecker(targetDir) { + + override fun checkReferences() { + val atomicfuDir = buildDir.resolve("classes/atomicfu/") + (if (atomicfuDir.exists() && atomicfuDir.isDirectory) atomicfuDir else buildDir).let { + it.walkBottomUp().filter { it.isFile && it.name.endsWith(".class") }.forEach { clazz -> + assertFalse(clazz.readBytes().eraseMetadata().findAtomicfuRef(), "Found kotlinx/atomicfu in class file ${clazz.path}") + } + } + } + + // The atomicfu compiler plugin does not remove atomic properties from metadata, + // so for now we check that there are no ATOMIC_FU_REF left in the class bytecode excluding metadata. + // This may be reverted after the fix in the compiler plugin transformer (See #254). + private fun ByteArray.eraseMetadata(): ByteArray { + val cw = ClassWriter(ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES) + ClassReader(this).accept(object : ClassVisitor(Opcodes.ASM9, cw) { + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + return if (descriptor == KOTLIN_METADATA_DESC) null else super.visitAnnotation(descriptor, visible) + } + }, ClassReader.SKIP_FRAMES) + return cw.toByteArray() + } +} + +internal fun GradleBuild.buildAndCheckBytecode() { + val buildResult = build() + require(buildResult.isSuccessful) { "Build of the project $projectName failed:\n ${buildResult.output}" } + BytecodeChecker(this.targetDir).checkReferences() +} + diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt new file mode 100644 index 00000000..a2ff7320 --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.gradle.plugin.test.framework.checker + +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.BuildResult +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.GradleBuild +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.atomicfuVersion +import kotlinx.atomicfu.gradle.plugin.test.framework.runner.dependencies + +private const val COMPILE_CLASSPATH = "compileClasspath" +private const val RUNTIME_CLASSPATH = "runtimeClasspath" +private val jvmAtomicfuDependency = "org.jetbrains.kotlinx:atomicfu-jvm:$atomicfuVersion" +private val commonAtomicfuDependency = "org.jetbrains.kotlinx:atomicfu:$atomicfuVersion" + +private class DependenciesChecker( + private val buildResult: BuildResult, + private val dependencies: List +) { + fun checkCompileOnly(compileConfigurations: List, runtimeConfigurations: List) { + val compileClasspath = buildResult.getDependencies(compileConfigurations) + val runtimeClasspath = buildResult.getDependencies(runtimeConfigurations) + for (dep in dependencies) { + check(compileClasspath.contains(dep)) { "Expected compileOnly dependency $dep was not found in the compileClasspath: $compileClasspath" } + check(!runtimeClasspath.contains(dep)) { "Dependency $dep should be compileOnly, but it was found in the runtimeClasspath: $runtimeClasspath" } + } + } + + fun checkImplementation(runtimeConfigurations: List) { + val runtimeClasspath = buildResult.getDependencies(runtimeConfigurations) + for (dep in dependencies) { + check(runtimeClasspath.contains(dep)) { "Expected implementation dependency $dep was not found in the runtimeClasspath: $runtimeClasspath" } + } + } +} + +// Checks that a simple non-mpp JVM project does not have atomicfu-jvm dependency in runtime classpath +internal fun GradleBuild.checkJvmCompileOnlyDependencies() { + val checker = DependenciesChecker(dependencies(), listOf(jvmAtomicfuDependency)) + checker.checkCompileOnly(listOf(COMPILE_CLASSPATH), listOf(RUNTIME_CLASSPATH)) +} + +// For MPP project with a JVM target and enabled atomicfu transformation checks that atomicfu-jvm is a compileOnly dependency +internal fun GradleBuild.checkMppJvmCompileOnlyDependencies() { + val checker = DependenciesChecker(dependencies(), listOf(jvmAtomicfuDependency)) + checker.checkCompileOnly( + compileConfigurations = listOf("jvmCompileClasspath"), + runtimeConfigurations = listOf("jvmRuntimeClasspath") + ) +} diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/BuildRunner.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/BuildRunner.kt new file mode 100644 index 00000000..fcc418ef --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/BuildRunner.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.gradle.plugin.test.framework.runner + +import java.io.File +import java.nio.file.Files + +internal interface GradleBuild { + val projectName: String + + val targetDir: File + + var enableJvmIrTransformation: Boolean + + var enableJsIrTransformation: Boolean + + var enableNativeIrTransformation: Boolean + + fun runGradle(commands: List): BuildResult + + fun clear() +} + +private class GradleBuildImpl( + override val projectName: String, + override val targetDir: File +) : GradleBuild { + + override var enableJvmIrTransformation = false + override var enableJsIrTransformation = false + override var enableNativeIrTransformation = false + + private val properties + get() = buildList { + add("-P$ENABLE_JVM_IR_TRANSFORMATION=$enableJvmIrTransformation") + add("-P$ENABLE_JS_IR_TRANSFORMATION=$enableJsIrTransformation") + add("-P$ENABLE_NATIVE_IR_TRANSFORMATION=$enableNativeIrTransformation") + } + + private var runCount = 0 + + override fun runGradle(commands: List): BuildResult = + buildGradleByShell(runCount++, commands, properties) + + override fun clear() { targetDir.deleteRecursively() } +} + +internal class BuildResult(exitCode: Int, private val logFile: File) { + val isSuccessful: Boolean = exitCode == 0 + + val output: String by lazy { logFile.readText() } + + // Gets the list of dependencies for every configuration + fun getDependencies(configurations: List): List { + val lines = output.lines() + val result = mutableListOf() + var index = 0 + while (index < lines.size) { + val line = lines[index++] + if (line.takeWhile { it.isLetter() } in configurations) break + } + while(index < lines.size) { + val line = lines[index++] + if (line.isBlank()) break + // trim leading indentations (\---) and symbols in the end (*): + // \--- org.jetbrains.kotlinx:atomicfu:0.22.0-SNAPSHOT (n) + result.add(line.dropWhile { !it.isLetter() }.substringBefore(" ")) + } + return result + } +} + +internal fun createGradleBuildFromSources(projectName: String): GradleBuild { + val projectDir = projectExamplesDir.resolve(projectName) + val targetDir = Files.createTempDirectory("${projectName.substringAfterLast('/')}-").toFile().apply { + projectDir.copyRecursively(this) + } + return GradleBuildImpl(projectName, targetDir) +} \ No newline at end of file diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Commands.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Commands.kt new file mode 100644 index 00000000..254b4a23 --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Commands.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.gradle.plugin.test.framework.runner + +internal fun GradleBuild.clean(): BuildResult = runGradle(listOf("clean")) + +internal fun GradleBuild.build(): BuildResult = runGradle(listOf("clean", "build")) + +internal fun GradleBuild.dependencies(): BuildResult = runGradle(listOf("dependencies")) + diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Environment.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Environment.kt new file mode 100644 index 00000000..cb33cf0c --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Environment.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.gradle.plugin.test.framework.runner + +import java.io.File + +internal const val ENABLE_JVM_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJvmIrTransformation" +internal const val ENABLE_JS_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJsIrTransformation" +internal const val ENABLE_NATIVE_IR_TRANSFORMATION = "kotlinx.atomicfu.enableNativeIrTransformation" + +// todo: get from System.properties +internal val atomicfuVersion = "0.22.0-SNAPSHOT" + +internal val gradleWrapperDir = File("..") + +internal val projectExamplesDir = File("examples") + +internal enum class Platform(val prefix: String) { JVM("jvm"), JS("js"), NATIVE("") } // todo pass platform name for Native platform diff --git a/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Utils.kt b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Utils.kt new file mode 100644 index 00000000..87df0614 --- /dev/null +++ b/integration-testing/src/test/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/runner/Utils.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.gradle.plugin.test.framework.runner + +import java.io.File + +internal fun GradleBuild.buildGradleByShell( + runIndex: Int, + commands: List, + properties: List +): BuildResult { + val logFile = targetDir.resolve("build-$runIndex.log") + + val gradleCommands = buildSystemCommand(targetDir, commands, properties) + + val builder = ProcessBuilder(gradleCommands) + builder.directory(gradleWrapperDir) + builder.redirectErrorStream(true) + builder.redirectOutput(logFile) + val process = builder.start() + val exitCode = process.waitFor() + return BuildResult(exitCode, logFile) +} + +private fun buildSystemCommand(projectDir: File, commands: List, properties: List): List { + return if (isWindows) + listOf("cmd", "/C", "gradlew.bat", "-p", projectDir.canonicalPath) + commands + properties + else + listOf("/bin/bash", "gradlew", "-p", projectDir.canonicalPath) + commands + properties +} + +private val isWindows: Boolean = System.getProperty("os.name")!!.contains("Windows") \ No newline at end of file