diff options
author | Sadaf Ebrahimi <sadafebrahimi@google.com> | 2022-11-01 20:37:16 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-11-01 20:37:16 +0000 |
commit | 2fe11a0356c22378a2b1e690bc9ee456a44ba71a (patch) | |
tree | 1e310cb8cd77f2e9ec4f1e3728eaffb60a8a4666 | |
parent | 8f4851d32831c91ff4f83e8c31242d5671e70305 (diff) | |
parent | 2282b1ddcdfb4f5c24fbbe0ce5f831b9ceaf683f (diff) | |
download | kotlinx.atomicfu-2fe11a0356c22378a2b1e690bc9ee456a44ba71a.tar.gz |
Upgrade kotlinx.atomicfu to 0.18.5 am: b943793454 am: 2282b1ddcdandroid-u-beta-1-gpl
Original change: https://android-review.googlesource.com/c/platform/external/kotlinx.atomicfu/+/2281219
Change-Id: I313dfddc8f437b3e1f4070bb15ea94d1a52578d8
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
58 files changed, 1866 insertions, 1737 deletions
@@ -1,5 +1,75 @@ # Change log for kotlinx.atomicfu +# Version 0.18.5 + +* Support JVM IR compiler plugin (#246). +* Update Kotlin to 1.7.20. +* Added more tests for atomicfu-gradle-plugin (#255). + +# Version 0.18.4 + +* Fix KGP compatibility bug with freeCompilerArgs modification (#247). +* Update kotlinx.metadata to 0.5.0 (#245). +* Update gradle version to 6.8.3 (#244) + +# Version 0.18.3 + +* Fix for atomicfu-gradle-plugin application to the MPP project (for Kotlin 1.7.20). + +# Version 0.18.2 + +* In Kotlin 1.7.10 the name of `atomicfu-runtime` module was reverted back to `kotlinx-atomicfu-runtime`, + as the renaming was an incompatible change. + Fixed `atomicfu-gradle-plugin` to add `kotlinx-atomicfu-runtime` dependency directly. + +# Version 0.18.1 + +* Fix for the compatibility issue: add `atomicfu-runtime` dependency directly since Kotlin 1.7.10. + +# Version 0.18.0 + +* Update Kotlin to 1.7.0. +* Fix kotlin 1.7 compatibility (#222). +* Update JVM target to 1.8 (see KT-45165). +* Fix for parsing Kotlin version in AtomicfuGradlePlugin. + +# Version 0.17.3 + +* Adding compiler plugin dependency only for projects with KGP >= 1.6.20 (#226). +* Compiler plugin runtime dependency fixes (#230). +* Update README badges (#228). + +# Version 0.17.2 + +* Update Kotlin to 1.6.20. +* IR transformation for Kotlin/JS. (#215). +* Update ASM to 9.3 for Java 18 support (#223) +* Update kotlinx.metadata to 0.4.2. + +# Version 0.17.1 + +* Support of `org.jetbrains.kotlin.js` plugin (#218). +* Fixed configuration cache bug. (#216). +* Bug fixes for delegated fields support (#179). + +# Version 0.17.0 + +* Update Kotlin to 1.6.0. +* Update ASM minimal api version to ASM7 (#203). +* Add explicit module-info for JPMS compatibility (#201). + +# Version 0.16.3 + +* Kotlin is updated to 1.5.30. +* All references to Bintray are removed from artefacts POMs. +* Added new Apple Silicon targets for K/N. + +# Version 0.16.2 + +* Update Kotlin to 1.5.20. +* ASM 9.1 for Java 15+ support (#190). +* Removing extra atomicfu references from LVT. + # Version 0.16.0 * Update Kotlin to 1.5.0. @@ -1,3 +1,7 @@ +# This project was upgraded with external_updater. +# Usage: tools/external_updater/updater.sh update kotlinx.atomicfu +# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md + name: "kotlinx.atomicfu" description: "The idiomatic way to use atomic operations in Kotlin." third_party { @@ -9,11 +13,11 @@ third_party { type: GIT value: "https://github.com/Kotlin/kotlinx.atomicfu" } - version: "0.14.4" + version: "0.18.5" license_type: NOTICE last_upgrade_date { - year: 2020 + year: 2022 month: 11 - day: 25 + day: 1 } } @@ -1,30 +1,49 @@ # AtomicFU -[![JetBrains incubator project](https://jb.gg/badges/incubator.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) +[![Kotlin Beta](https://kotl.in/badges/beta.svg)](https://kotlinlang.org/docs/components-stability.html) +[![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.atomicfu/images/download.svg) ](https://bintray.com/kotlin/kotlinx/kotlinx.atomicfu/_latestVersion) - -The idiomatic way to use atomic operations in Kotlin. - -* Code it like `AtomicReference/Int/Long`, but run it in production efficiently as `AtomicXxxFieldUpdater` on Kotlin/JVM - and as plain unboxed values on Kotlin/JS. -* Use Kotlin-specific extensions (e.g. inline `updateAndGet` and `getAndUpdate` functions). -* Compile-time dependency only (no runtime dependencies). - * Post-compilation bytecode transformer that declares all the relevant field updaters for you on [Kotlin/JVM](#jvm). - * Post-compilation JavaScript files transformer on [Kotlin/JS](#js). -* Multiplatform: - * [Kotlin/Native](#native) is supported. - * However, Kotlin/Native works as library dependency at the moment (unlike Kotlin/JVM and Kotlin/JS). - * This enables writing [common](#common) Kotlin code with atomics that compiles for JVM, JS, and Native. -* [Gradle](#gradle-build-setup) for all platforms and [Maven](#maven-build-setup) for JVM are supported. -* [Additional features](#additional-features) include: - * [JDK9 VarHandle](#varhandles-with-java-9). - * [Arrays of atomic values](#arrays-of-atomic-values). - * [User-defined extensions on atomics](#user-defined-extensions-on-atomics) - * [Locks](#locks) - * [Testing of lock-free data structures](#testing-lock-free-data-structures-on-jvm). - * [Tracing operations](#tracing-operations) - +[![Maven Central](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/atomicfu)](https://search.maven.org/artifact/org.jetbrains.kotlinx/atomicfu/0.18.5/pom) + +>Note on Beta status: the plugin is in its active development phase and changes from release to release. +>We do provide a compatibility of atomicfu-transformed artifacts between releases, but we do not provide +>strict compatibility guarantees on plugin API and its general stability between Kotlin versions. + +**Atomicfu** is a multiplatform library that provides the idiomatic and effective way of using atomic operations in Kotlin. + +## Table of contents +- [Features](#features) +- [Example](#example) +- [Quickstart](#quickstart) + - [Apply plugin to a project](#apply-plugin) + - [Gradle configuration](#gradle-configuration) + - [Maven configuration](#maven-configuration) +- [Usage constraints](#usage-constraints) +- [Transformation modes](#transformation-modes) + - [Atomicfu compiler plugin](#atomicfu-compiler-plugin) +- [Options for post-compilation transformation](#options-for-post-compilation-transformation) + - [JVM options](#jvm-options) + - [JS options](#js-options) +- [More features](#more-features) + - [Arrays of atomic values](#arrays-of-atomic-values) + - [User-defined extensions on atomics](#user-defined-extensions-on-atomics) + - [Locks](#locks) + - [Tracing operations](#tracing-operations) +- [Kotlin/Native support](#kotlin-native-support) + + +## Features + +* Code it like a boxed value `atomic(0)`, but run it in production efficiently: + * as `java.util.concurrent.atomic.AtomicXxxFieldUpdater` on Kotlin/JVM + * as a plain unboxed value on Kotlin/JS +* Multiplatform: write common Kotlin code with atomics that compiles for Kotlin JVM, JS, and Native backends: + * Compile-only dependency for JVM and JS (no runtime dependencies) + * Compile and runtime dependency for Kotlin/Native +* Use Kotlin-specific extensions (e.g. inline `loop`, `update`, `updateAndGet` functions). +* Use atomic arrays, user-defined extensions on atomics and locks (see [more features](#more-features)). +* [Tracing operations](#tracing-operations) for debugging. + ## Example Let us declare a `top` variable for a lock-free stack implementation: @@ -74,107 +93,67 @@ val myLong = atomic(0L) // note: long initial value Integer and long atomics provide all the usual `getAndIncrement`, `incrementAndGet`, `getAndAdd`, `addAndGet`, and etc operations. They can be also atomically modified via `+=` and `-=` operators. -## Dos and Don'ts - -* Declare atomic variables as `private val` or `internal val`. You can use just (public) `val`, - but make sure they are not directly accessed outside of your Kotlin module (outside of the source set). - Access to the atomic variable itself shall be encapsulated. -* Only simple operations on atomic variables _directly_ are supported. - * Do not read references on atomic variables into local variables, - e.g. `top.compareAndSet(...)` is Ok, while `val tmp = top; tmp...` is not. - * Do not leak references on atomic variables in other way (return, pass as params, etc). -* Do not introduce complex data flow in parameters to atomic variable operations, - i.e. `top.value = complex_expression` and `top.compareAndSet(cur, complex_expression)` are not supported - (more specifically, `complex_expression` should not have branches in its compiled representation). - Extract `complex_expression` into a variable when needed. -* Use the following convention if you need to expose the value of atomic property to the public: - -```kotlin -private val _foo = atomic<T>(initial) // private atomic, convention is to name it with leading underscore -public var foo: T by _foo // public delegated property (val/var) -``` - -## Gradle build setup +## Quickstart +### Apply plugin +#### Gradle configuration -Building with Gradle is supported for all platforms. +Gradle configuration is supported for all platforms, minimal version is Gradle 6.8. -### JVM +In top-level build file: -You will need Gradle 4.10 or later. -Add and apply AtomicFU plugin. It adds all the corresponding dependencies -and transformations automatically. -See [additional configuration](#additional-configuration) if that needs tweaking. +<details open> +<summary>Kotlin</summary> -```groovy +```kotlin buildscript { - ext.atomicfu_version = '0.16.0' + repositories { + mavenCentral() + } dependencies { - classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version" + classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.18.5") } } -apply plugin: 'kotlinx-atomicfu' +apply(plugin = "kotlinx-atomicfu") ``` +</details> -### JS - -Configure add apply plugin just like for [JVM](#jvm). - -### Native - -This library is available for Kotlin/Native (`atomicfu-native`). -Kotlin/Native uses Gradle metadata and needs Gradle version 5.3 or later. -See [Gradle Metadata 1.0 announcement](https://blog.gradle.org/gradle-metadata-1.0) for more details. -Apply the corresponding plugin just like for [JVM](#jvm). - -Atomic references for Kotlin/Native are based on -[FreezableAtomicReference](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-freezable-atomic-reference/-init-.html) -and every reference that is stored to the previously -[frozen](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/freeze.html) -(shared with another thread) atomic is automatically frozen, too. - -Since Kotlin/Native does not generally provide binary compatibility between versions, -you should use the same version of Kotlin compiler as was used to build AtomicFU. -See [gradle.properties](gradle.properties) in AtomicFU project for its `kotlin_version`. - -### Common - -If you write a common code that should get compiled or different platforms, add `org.jetbrains.kotlinx:atomicfu-common` -to your common code dependencies or apply `kotlinx-atomicfu` plugin that adds this dependency automatically: +<details> +<summary>Groovy</summary> ```groovy -dependencies { - compile "org.jetbrains.kotlinx:atomicfu-common:$atomicfu_version" +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.18.5' + } } + +apply plugin: 'kotlinx-atomicfu' ``` +</details> -### Additional configuration - -There are the following additional parameters (with their defaults): +#### Maven configuration -```groovy -atomicfu { - dependenciesVersion = '0.16.0' // set to null to turn-off auto dependencies - transformJvm = true // set to false to turn off JVM transformation - transformJs = true // set to false to turn off JS transformation - variant = "FU" // JVM transformation variant: FU,VH, or BOTH - verbose = false // set to true to be more verbose -} -``` +Maven configuration is supported for JVM projects. -## Maven build setup -Declare AtomicFU version: +<details open> +<summary>Declare atomicfu version</summary> ```xml <properties> - <atomicfu.version>0.16.0</atomicfu.version> + <atomicfu.version>0.18.5</atomicfu.version> </properties> ``` -Declare _provided_ dependency on the AtomicFU library -(the users of the resulting artifact will not have a dependency on AtomicFU library): +</details> + +<details> +<summary>Declare provided dependency on the AtomicFU library</summary> ```xml <dependencies> @@ -187,71 +166,161 @@ Declare _provided_ dependency on the AtomicFU library </dependencies> ``` +</details> + Configure build steps so that Kotlin compiler puts classes into a different `classes-pre-atomicfu` directory, which is then transformed to a regular `classes` directory to be used later by tests and delivery. +<details> +<summary>Build steps</summary> + ```xml <build> - <plugins> - <!-- compile Kotlin files to staging directory --> - <plugin> - <groupId>org.jetbrains.kotlin</groupId> - <artifactId>kotlin-maven-plugin</artifactId> - <version>${kotlin.version}</version> - <executions> - <execution> - <id>compile</id> - <phase>compile</phase> - <goals> - <goal>compile</goal> - </goals> - <configuration> - <output>${project.build.directory}/classes-pre-atomicfu</output> - <!-- "VH" to use Java 9 VarHandle, "BOTH" to produce multi-version code --> - <variant>FU</variant> - </configuration> - </execution> - </executions> - </plugin> - <!-- transform classes with AtomicFU plugin --> - <plugin> - <groupId>org.jetbrains.kotlinx</groupId> - <artifactId>atomicfu-maven-plugin</artifactId> - <version>${atomicfu.version}</version> - <executions> - <execution> - <goals> - <goal>transform</goal> - </goals> - <configuration> - <input>${project.build.directory}/classes-pre-atomicfu</input> - </configuration> - </execution> - </executions> - </plugin> - </plugins> + <plugins> + <!-- compile Kotlin files to staging directory --> + <plugin> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-maven-plugin</artifactId> + <version>${kotlin.version}</version> + <executions> + <execution> + <id>compile</id> + <phase>compile</phase> + <goals> + <goal>compile</goal> + </goals> + <configuration> + <output>${project.build.directory}/classes-pre-atomicfu</output> + </configuration> + </execution> + </executions> + </plugin> + <!-- transform classes with AtomicFU plugin --> + <plugin> + <groupId>org.jetbrains.kotlinx</groupId> + <artifactId>atomicfu-maven-plugin</artifactId> + <version>${atomicfu.version}</version> + <executions> + <execution> + <goals> + <goal>transform</goal> + </goals> + <configuration> + <input>${project.build.directory}/classes-pre-atomicfu</input> + <!-- "VH" to use Java 9 VarHandle, "BOTH" to produce multi-version code --> + <variant>FU</variant> + </configuration> + </execution> + </executions> + </plugin> + </plugins> </build> ``` -## Additional features +</details> -AtomicFU provides some additional features that you can optionally use. +## Usage constraints -### VarHandles with Java 9 +* Declare atomic variables as `private val` or `internal val`. You can use just (public) `val`, + but make sure they are not directly accessed outside of your Kotlin module (outside of the source set). + Access to the atomic variable itself shall be encapsulated. +* Only simple operations on atomic variables _directly_ are supported. + * Do not read references on atomic variables into local variables, + e.g. `top.compareAndSet(...)` is ok, while `val tmp = top; tmp...` is not. + * Do not leak references on atomic variables in other way (return, pass as params, etc). +* Do not introduce complex data flow in parameters to atomic variable operations, + i.e. `top.value = complex_expression` and `top.compareAndSet(cur, complex_expression)` are not supported + (more specifically, `complex_expression` should not have branches in its compiled representation). + Extract `complex_expression` into a variable when needed. +* Use the following convention if you need to expose the value of atomic property to the public: + +```kotlin +private val _foo = atomic<T>(initial) // private atomic, convention is to name it with leading underscore +public var foo: T by _foo // public delegated property (val/var) +``` + +## Transformation modes + +Basically, Atomicfu library provides an effective usage of atomic values by performing the transformations of the compiled code. +For JVM and JS there 2 transformation modes available: +* **Post-compilation transformation** that modifies the compiled bytecode or `*.js` files. +* **IR transformation** that is performed by the atomicfu compiler plugin. + +### Atomicfu compiler plugin + +Compiler plugin transformation is less fragile than transformation of the compiled sources +as it depends on the compiler IR tree. + +To turn on IR transformation set these properties in your `gradle.properties` file: + +<details open> +<summary>For Kotlin >= 1.7.20</summary> + +```groovy +kotlinx.atomicfu.enableJvmIrTransformation=true // for JVM IR transformation +kotlinx.atomicfu.enableJsIrTransformation=true // for JS IR transformation +``` + +</details> + +<details> + + +<summary> For Kotlin >= 1.6.20 and Kotlin < 1.7.20</summary> + +```groovy +kotlinx.atomicfu.enableIrTransformation=true // only JS IR transformation is supported +``` + +</details> + +Also for JS backend make sure that `ir` or `both` compiler mode is set: -AtomicFU can produce code that uses Java 9 -[VarHandle](https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/VarHandle.html) -instead of `AtomicXxxFieldUpdater`. Configure transformation `variant` in Gradle build file: - +```groovy +kotlin.js.compiler=ir // or both +``` + + +## Options for post-compilation transformation + +Some configuration options are available for _post-compilation transform tasks_ on JVM and JS. + +To set configuration options you should create `atomicfu` section in a `build.gradle` file, +like this: ```groovy atomicfu { - variant = "VH" + dependenciesVersion = '0.18.5' } -``` - -It can also create [JEP 238](https://openjdk.java.net/jeps/238) multi-release jar file with both -`AtomicXxxFieldUpdater` for JDK<=8 and `VarHandle` for for JDK9+ if you -set `variant` to `"BOTH"`. +``` + +### JVM options + +To turn off transformation for Kotlin/JVM set option `transformJvm` to `false`. + +Configuration option `jvmVariant` defines the Java class that replaces atomics during bytecode transformation. +Here are the valid options: +- `FU` – atomics are replaced with [AtomicXxxFieldUpdater](https://docs.oracle.com/javase/10/docs/api/java/util/concurrent/atomic/AtomicIntegerFieldUpdater.html). +- `VH` – atomics are replaced with [VarHandle](https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/VarHandle.html), + this option is supported for JDK 9+. +- `BOTH` – [multi-release jar file](https://openjdk.java.net/jeps/238) will be created with both `AtomicXxxFieldUpdater` for JDK <= 8 and `VarHandle` for JDK 9+. + +### JS options + +To turn off transformation for Kotlin/JS set option `transformJs` to `false`. + +Here are all available configuration options (with their defaults): +```groovy +atomicfu { + dependenciesVersion = '0.18.5' // set to null to turn-off auto dependencies + transformJvm = true // set to false to turn off JVM transformation + jvmVariant = "FU" // JVM transformation variant: FU,VH, or BOTH + transformJs = true // set to false to turn off JVM transformation +} +``` + +## More features + +AtomicFU provides some additional features that you can use. ### Arrays of atomic values @@ -297,46 +366,9 @@ use `lock`/`tryLock`/`unlock` functions or `lock.withLock { ... }` extension fun [jucl.ReentrantLock](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html) is used on JVM. On JVM it is a typealias to the later class, erased on JS. -Condition variables (`notify`/`wait` and `signal`/`await`) are not supported. - -### Testing lock-free data structures on JVM - -You can optionally test lock-freedomness of lock-free data structures using -[`LockFreedomTestEnvironment`](atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/LockFreedomTestEnvironment.kt) class. -See example in [`LockFreeQueueLFTest`](atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/test/LockFreeQueueLFTest.kt). -Testing is performed by pausing one (random) thread before or after a random state-update operation and -making sure that all other threads can still make progress. - -In order to make those test to actually perform lock-freedomness testing you need to configure an additional -execution of tests with the original (non-transformed) classes for Maven: - -```xml -<build> - <plugins> - <!-- additional test execution with surefire on non-transformed files --> - <plugin> - <artifactId>maven-surefire-plugin</artifactId> - <executions> - <execution> - <id>lockfree-test</id> - <phase>test</phase> - <goals> - <goal>test</goal> - </goals> - <configuration> - <classesDirectory>${project.build.directory}/classes-pre-atomicfu</classesDirectory> - <includes> - <include>**/*LFTest.*</include> - </includes> - </configuration> - </execution> - </executions> - </plugin> - </plugins> -</build> -``` - -For Gradle there is nothing else to add. Tests are always run using original (non-transformed) classes. +> Note that package `kotlinx.atomicfu.locks` is experimental explicitly even while atomicfu is experimental itself, +> meaning that no ABI guarantees are provided whatsoever. API from this package is not recommended to use in libraries +> that other projects depend on. ### Tracing operations @@ -369,3 +401,15 @@ private val trace = Trace(size = 64) { `trace` is only seen before transformation and completely erased after on Kotlin/JVM and Kotlin/JS. +## Kotlin Native support + +Atomic references for Kotlin/Native are based on +[FreezableAtomicReference](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-freezable-atomic-reference/-init-.html) +and every reference that is stored to the previously +[frozen](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/freeze.html) +(shared with another thread) atomic is automatically frozen, too. + +Since Kotlin/Native does not generally provide binary compatibility between versions, +you should use the same version of Kotlin compiler as was used to build AtomicFU. +See [gradle.properties](gradle.properties) in AtomicFU project for its `kotlin_version`. + @@ -51,7 +51,7 @@ To release new `<version>` of `kotlinx-atomicfu`: * Create a release named `<version>`. * Cut & paste lines from [`CHANGES.md`](CHANGES.md) into description. -13. In [Sonatype](oss.sonatype.org/#stagingRepositories) admin interface: +13. In [Nexus](https://oss.sonatype.org/#stagingRepositories) admin interface: * Close the repository and wait for it to verify. * Release it. diff --git a/atomicfu-gradle-plugin/build.gradle b/atomicfu-gradle-plugin/build.gradle index 28091b6..5312551 100644 --- a/atomicfu-gradle-plugin/build.gradle +++ b/atomicfu-gradle-plugin/build.gradle @@ -14,26 +14,41 @@ if (rootProject.ext.jvm_ir_enabled) { // Gradle plugin must be compiled targeting the same Kotlin version as used by Gradle kotlin.sourceSets.all { languageSettings { - apiVersion = "1.3" - languageVersion = "1.3" + apiVersion = "1.4" + languageVersion = "1.4" } } dependencies { - compile gradleApi() - compile project(":atomicfu-transformer") - compile 'org.jetbrains.kotlin:kotlin-stdlib' + implementation(project(":atomicfu-transformer")) { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib' + } + compileOnly gradleApi() + compileOnly 'org.jetbrains.kotlin:kotlin-stdlib' compileOnly "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + // atomicfu compiler plugin dependency will be loaded to kotlinCompilerPluginClasspath + implementation "org.jetbrains.kotlin:atomicfu:$kotlin_version" - testCompile gradleTestKit() - testCompile 'org.jetbrains.kotlin:kotlin-test' - testCompile 'org.jetbrains.kotlin:kotlin-test-junit' - testCompile 'junit:junit:4.12' + testImplementation gradleTestKit() + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.jetbrains.kotlin:kotlin-test-junit' + testImplementation 'junit:junit:4.12' } configurations { - testPluginClasspath + testPluginClasspath { + attributes { + attribute( + Usage.USAGE_ATTRIBUTE, + project.objects.named(Usage.class, Usage.JAVA_RUNTIME) + ) + attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.class, Category.LIBRARY) + ) + } + } } dependencies { diff --git a/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt b/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt index bb619a5..b77e95b 100644 --- a/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt +++ b/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt @@ -14,22 +14,33 @@ import org.gradle.api.tasks.compile.* import org.gradle.api.tasks.testing.* import org.gradle.jvm.tasks.* import org.jetbrains.kotlin.gradle.dsl.* +import org.jetbrains.kotlin.gradle.dsl.KotlinCompile import org.jetbrains.kotlin.gradle.plugin.* import java.io.* import java.util.* import java.util.concurrent.* +import org.jetbrains.kotlin.gradle.targets.js.* +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget +import org.jetbrains.kotlin.gradle.tasks.* +import org.jetbrains.kotlinx.atomicfu.gradle.* private const val EXTENSION_NAME = "atomicfu" private const val ORIGINAL_DIR_NAME = "originalClassesDir" private const val COMPILE_ONLY_CONFIGURATION = "compileOnly" private const val IMPLEMENTATION_CONFIGURATION = "implementation" private const val TEST_IMPLEMENTATION_CONFIGURATION = "testImplementation" +// If the project uses KGP <= 1.6.20, only JS IR compiler plugin is available, and it is turned on via setting this property. +// The property is supported for backwards compatibility. +private const val ENABLE_JS_IR_TRANSFORMATION_LEGACY = "kotlinx.atomicfu.enableIrTransformation" +private const val ENABLE_JS_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJsIrTransformation" +private const val ENABLE_JVM_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJvmIrTransformation" open class AtomicFUGradlePlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val pluginVersion = rootProject.buildscript.configurations.findByName("classpath") ?.allDependencies?.find { it.name == "atomicfu-gradle-plugin" }?.version extensions.add(EXTENSION_NAME, AtomicFUPluginExtension(pluginVersion)) + applyAtomicfuCompilerPlugin() configureDependencies() configureTasks() } @@ -43,12 +54,13 @@ private fun Project.configureDependencies() { ) dependencies.add(TEST_IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JVM, version)) } - withPluginWhenEvaluatedDependencies("kotlin2js") { version -> + withPluginWhenEvaluatedDependencies("org.jetbrains.kotlin.js") { version -> dependencies.add( if (config.transformJs) COMPILE_ONLY_CONFIGURATION else IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JS, version) ) dependencies.add(TEST_IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JS, version)) + addCompilerPluginDependency() } withPluginWhenEvaluatedDependencies("kotlin-multiplatform") { version -> configureMultiplatformPluginDependencies(version) @@ -59,7 +71,9 @@ private fun Project.configureTasks() { val config = config withPluginWhenEvaluated("kotlin") { if (config.transformJvm) { - configureTransformTasks("compileTestKotlin") { sourceSet, transformedDir, originalDir -> + // skip transformation task if ir transformation is enabled + if (rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION)) return@withPluginWhenEvaluated + configureJvmTransformation("compileTestKotlin") { sourceSet, transformedDir, originalDir -> createJvmTransformTask(sourceSet).configureJvmTask( sourceSet.compileClasspath, sourceSet.classesTaskName, @@ -70,20 +84,88 @@ private fun Project.configureTasks() { } } } - withPluginWhenEvaluated("kotlin2js") { - if (config.transformJs) { - configureTransformTasks("compileTestKotlin2Js") { sourceSet, transformedDir, originalDir -> - createJsTransformTask(sourceSet).configureJsTask( - sourceSet.classesTaskName, - transformedDir, - originalDir, - config - ) + withPluginWhenEvaluated("org.jetbrains.kotlin.js") { + if (config.transformJs) configureJsTransformation() + } + withPluginWhenEvaluated("kotlin-multiplatform") { + configureMultiplatformTransformation() + } +} + +private data class KotlinVersion(val major: Int, val minor: Int, val patch: Int) + +private fun Project.getKotlinVersion(): KotlinVersion { + val kotlinVersion = getKotlinPluginVersion() + val (major, minor) = kotlinVersion + .split('.') + .take(2) + .map { it.toInt() } + val patch = kotlinVersion.substringAfterLast('.').substringBefore('-').toInt() + return KotlinVersion(major, minor, patch) +} + +private fun KotlinVersion.atLeast(major: Int, minor: Int, patch: Int) = + this.major == major && (this.minor == minor && this.patch >= patch || this.minor > minor) || this.major > major + +// kotlinx-atomicfu compiler plugin is available for KGP >= 1.6.20 +private fun Project.isCompilerPluginAvailable() = getKotlinVersion().atLeast(1, 6, 20) + +private fun Project.applyAtomicfuCompilerPlugin() { + val kotlinVersion = getKotlinVersion() + // for KGP >= 1.7.20: + // compiler plugin for JS IR is applied via the property `kotlinx.atomicfu.enableJsIrTransformation` + // compiler plugin for JVM IR is applied via the property `kotlinx.atomicfu.enableJvmIrTransformation` + if (kotlinVersion.atLeast(1, 7, 20)) { + plugins.apply(AtomicfuKotlinGradleSubplugin::class.java) + extensions.getByType(AtomicfuKotlinGradleSubplugin.AtomicfuKotlinGradleExtension::class.java).apply { + isJsIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION) + isJvmIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION) + } + } else { + // for KGP >= 1.6.20 && KGP <= 1.7.20: + // compiler plugin for JS IR is applied via the property `kotlinx.atomicfu.enableIrTransformation` + // compiler plugin for JVM IR is not supported yet + if (kotlinVersion.atLeast(1, 6, 20)) { + if (rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION_LEGACY)) { + plugins.apply(AtomicfuKotlinGradleSubplugin::class.java) } } } - withPluginWhenEvaluated("kotlin-multiplatform") { - configureMultiplatformPluginTasks() +} + +private fun Project.getBooleanProperty(name: String) = + rootProject.findProperty(name)?.toString()?.toBooleanStrict() ?: false + +private fun String.toBooleanStrict(): Boolean = when (this) { + "true" -> true + "false" -> false + else -> throw IllegalArgumentException("The string doesn't represent a boolean value: $this") +} + +private fun Project.needsJsIrTransformation(target: KotlinTarget): Boolean = + (rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION) || rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION_LEGACY)) + && target.isJsIrTarget() + +private fun KotlinTarget.isJsIrTarget() = (this is KotlinJsTarget && this.irTarget != null) || this is KotlinJsIrTarget + +private fun Project.addCompilerPluginDependency() { + if (isCompilerPluginAvailable()) { + withKotlinTargets { target -> + if (needsJsIrTransformation(target)) { + target.compilations.forEach { kotlinCompilation -> + kotlinCompilation.dependencies { + if (getKotlinVersion().atLeast(1, 7, 10)) { + // since Kotlin 1.7.10 we can add `atomicfu-runtime` dependency directly + implementation("org.jetbrains.kotlin:kotlinx-atomicfu-runtime:${getKotlinPluginVersion()}") + } else { + // add atomicfu compiler plugin dependency + // to provide the `atomicfu-runtime` library used during compiler plugin transformation + implementation("org.jetbrains.kotlin:atomicfu:${getKotlinPluginVersion()}") + } + } + } + } + } } } @@ -135,88 +217,112 @@ fun Project.withPluginWhenEvaluatedDependencies(plugin: String, fn: Project.(ver } fun Project.withKotlinTargets(fn: (KotlinTarget) -> Unit) { - extensions.findByType(KotlinProjectExtension::class.java)?.let { kotlinExtension -> - val targetsExtension = (kotlinExtension as? ExtensionAware)?.extensions?.findByName("targets") - @Suppress("UNCHECKED_CAST") - val targets = targetsExtension as NamedDomainObjectContainer<KotlinTarget> + extensions.findByType(KotlinTargetsContainer::class.java)?.let { kotlinExtension -> // find all compilations given sourceSet belongs to - targets.all { target -> fn(target) } + kotlinExtension.targets + .all { target -> fn(target) } } } -private fun KotlinCommonOptions.addFriendPaths(friendPathsFileCollection: FileCollection) { - val argName = when (this) { - is KotlinJvmOptions -> "-Xfriend-paths" - is KotlinJsOptions -> "-Xfriend-modules" - else -> return +private fun KotlinCompile<*>.setFriendPaths(friendPathsFileCollection: FileCollection) { + val (majorVersion, minorVersion) = project.getKotlinPluginVersion() + .split('.') + .take(2) + .map { it.toInt() } + if (majorVersion == 1 && minorVersion < 7) { + (this as? AbstractKotlinCompile<*>)?.friendPaths?.from(friendPathsFileCollection) + } else { + // See KT-KT-54167 (works only for KGP 1.7.0+) + (this as BaseKotlinCompile).friendPaths.from(friendPathsFileCollection) } - freeCompilerArgs = freeCompilerArgs + "$argName=${friendPathsFileCollection.joinToString(",")}" } -fun Project.configureMultiplatformPluginTasks() { - val originalDirsByCompilation = hashMapOf<KotlinCompilation<*>, FileCollection>() - val config = config +fun Project.configureJsTransformation() = + configureTransformationForTarget((kotlinExtension as KotlinJsProjectExtension).js()) + +fun Project.configureMultiplatformTransformation() = withKotlinTargets { target -> if (target.platformType == KotlinPlatformType.common || target.platformType == KotlinPlatformType.native) { return@withKotlinTargets // skip the common & native targets -- no transformation for them } - target.compilations.all compilations@{ compilation -> - val compilationType = compilation.name.compilationNameToType() - ?: return@compilations // skip unknown compilations - val classesDirs = compilation.output.classesDirs - // make copy of original classes directory - val originalClassesDirs: FileCollection = - project.files(classesDirs.from.toTypedArray()).filter { it.exists() } - originalDirsByCompilation[compilation] = originalClassesDirs - val transformedClassesDir = - project.buildDir.resolve("classes/atomicfu/${target.name}/${compilation.name}") - val transformTask = when (target.platformType) { - KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> { - if (!config.transformJvm) return@compilations // skip when transformation is turned off - project.createJvmTransformTask(compilation).configureJvmTask( - compilation.compileDependencyFiles, - compilation.compileAllTaskName, - transformedClassesDir, - originalClassesDirs, - config - ) - } - KotlinPlatformType.js -> { - if (!config.transformJs) return@compilations // skip when transformation is turned off - project.createJsTransformTask(compilation).configureJsTask( - compilation.compileAllTaskName, - transformedClassesDir, - originalClassesDirs, - config - ) - } - else -> error("Unsupported transformation platform '${target.platformType}'") - } - //now transformTask is responsible for compiling this source set into the classes directory - classesDirs.setFrom(transformedClassesDir) - classesDirs.builtBy(transformTask) - (tasks.findByName(target.artifactsTaskName) as? Jar)?.apply { - setupJarManifest(multiRelease = config.variant.toVariant() == Variant.BOTH) + configureTransformationForTarget(target) + } + +private fun Project.configureTransformationForTarget(target: KotlinTarget) { + val originalDirsByCompilation = hashMapOf<KotlinCompilation<*>, FileCollection>() + val config = config + target.compilations.all compilations@{ compilation -> + val compilationType = compilation.name.compilationNameToType() + ?: return@compilations // skip unknown compilations + val classesDirs = compilation.output.classesDirs + // make copy of original classes directory + val originalClassesDirs: FileCollection = + project.files(classesDirs.from.toTypedArray()).filter { it.exists() } + originalDirsByCompilation[compilation] = originalClassesDirs + val transformedClassesDir = + project.buildDir.resolve("classes/atomicfu/${target.name}/${compilation.name}") + val transformTask = when (target.platformType) { + KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> { + // skip transformation task if transformation is turned off or ir transformation is enabled + if (!config.transformJvm || rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION)) return@compilations + project.createJvmTransformTask(compilation).configureJvmTask( + compilation.compileDependencyFiles, + compilation.compileAllTaskName, + transformedClassesDir, + originalClassesDirs, + config + ) } - // test should compile and run against original production binaries - if (compilationType == CompilationType.TEST) { - val mainCompilation = - compilation.target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) - val originalMainClassesDirs = project.files( - // use Callable because there is no guarantee that main is configured before test - Callable { originalDirsByCompilation[mainCompilation]!! } + KotlinPlatformType.js -> { + // skip when js transformation is not needed or when IR is transformed + if (!config.transformJs || (needsJsIrTransformation(target))) { + return@compilations + } + project.createJsTransformTask(compilation).configureJsTask( + compilation.compileAllTaskName, + transformedClassesDir, + originalClassesDirs, + config ) + } + else -> error("Unsupported transformation platform '${target.platformType}'") + } + //now transformTask is responsible for compiling this source set into the classes directory + classesDirs.setFrom(transformedClassesDir) + classesDirs.builtBy(transformTask) + (tasks.findByName(target.artifactsTaskName) as? Jar)?.apply { + setupJarManifest(multiRelease = config.jvmVariant.toJvmVariant() == JvmVariant.BOTH) + } + // test should compile and run against original production binaries + if (compilationType == CompilationType.TEST) { + val mainCompilation = + compilation.target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) + val originalMainClassesDirs = project.files( + // use Callable because there is no guarantee that main is configured before test + Callable { originalDirsByCompilation[mainCompilation]!! } + ) + // KGP >= 1.7.0 has breaking changes in task hierarchy: + // https://youtrack.jetbrains.com/issue/KT-32805#focus=Comments-27-5915479.0-0 + val (majorVersion, minorVersion) = getKotlinPluginVersion() + .split('.') + .take(2) + .map { it.toInt() } + if (majorVersion == 1 && minorVersion < 7) { (tasks.findByName(compilation.compileKotlinTaskName) as? AbstractCompile)?.classpath = originalMainClassesDirs + compilation.compileDependencyFiles - mainCompilation.output.classesDirs + } else { + (tasks.findByName(compilation.compileKotlinTaskName) as? AbstractKotlinCompileTool<*>) + ?.libraries + ?.setFrom( + originalMainClassesDirs + compilation.compileDependencyFiles - mainCompilation.output.classesDirs + ) + } - (tasks.findByName("${target.name}${compilation.name.capitalize()}") as? Test)?.classpath = - originalMainClassesDirs + (compilation as KotlinCompilationToRunnableFiles).runtimeDependencyFiles - mainCompilation.output.classesDirs + (tasks.findByName("${target.name}${compilation.name.capitalize()}") as? Test)?.classpath = + originalMainClassesDirs + (compilation as KotlinCompilationToRunnableFiles).runtimeDependencyFiles - mainCompilation.output.classesDirs - compilation.compileKotlinTask.doFirst { - compilation.kotlinOptions.addFriendPaths(originalMainClassesDirs) - } - } + compilation.compileKotlinTask.setFriendPaths(originalMainClassesDirs) } } } @@ -234,15 +340,16 @@ fun Project.sourceSetsByCompilation(): Map<KotlinSourceSet, List<KotlinCompilati } fun Project.configureMultiplatformPluginDependencies(version: String) { - if (rootProject.findProperty("kotlin.mpp.enableGranularSourceSetsMetadata").toString().toBoolean()) { + if (rootProject.getBooleanProperty("kotlin.mpp.enableGranularSourceSetsMetadata")) { + addCompilerPluginDependency() val mainConfigurationName = project.extensions.getByType(KotlinMultiplatformExtension::class.java).sourceSets - .getByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) - .compileOnlyConfigurationName + .getByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) + .compileOnlyConfigurationName dependencies.add(mainConfigurationName, getAtomicfuDependencyNotation(Platform.MULTIPLATFORM, version)) val testConfigurationName = project.extensions.getByType(KotlinMultiplatformExtension::class.java).sourceSets - .getByName(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) - .implementationConfigurationName + .getByName(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) + .implementationConfigurationName dependencies.add(testConfigurationName, getAtomicfuDependencyNotation(Platform.MULTIPLATFORM, version)) // For each source set that is only used in Native compilations, add an implementation dependency so that it @@ -258,20 +365,21 @@ fun Project.configureMultiplatformPluginDependencies(version: String) { } } else { sourceSetsByCompilation().forEach { (sourceSet, compilations) -> + addCompilerPluginDependency() val platformTypes = compilations.map { it.platformType }.toSet() val compilationNames = compilations.map { it.compilationName }.toSet() if (compilationNames.size != 1) error("Source set '${sourceSet.name}' of project '$name' is part of several compilations $compilationNames") val compilationType = compilationNames.single().compilationNameToType() - ?: return@forEach // skip unknown compilations + ?: return@forEach // skip unknown compilations val platform = - if (platformTypes.size > 1) Platform.MULTIPLATFORM else // mix of platform types -> "common" - when (platformTypes.single()) { - KotlinPlatformType.common -> Platform.MULTIPLATFORM - KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> Platform.JVM - KotlinPlatformType.js -> Platform.JS - KotlinPlatformType.native -> Platform.NATIVE - } + if (platformTypes.size > 1) Platform.MULTIPLATFORM else // mix of platform types -> "common" + when (platformTypes.single()) { + KotlinPlatformType.common -> Platform.MULTIPLATFORM + KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> Platform.JVM + KotlinPlatformType.js -> Platform.JS + KotlinPlatformType.native, KotlinPlatformType.wasm -> Platform.NATIVE + } val configurationName = when { // impl dependency for native (there is no transformation) platform == Platform.NATIVE -> sourceSet.implementationConfigurationName @@ -285,7 +393,7 @@ fun Project.configureMultiplatformPluginDependencies(version: String) { } } -fun Project.configureTransformTasks( +fun Project.configureJvmTransformation( testTaskName: String, createTransformTask: (sourceSet: SourceSet, transformedDir: File, originalDir: FileCollection) -> Task ) { @@ -305,7 +413,7 @@ fun Project.configureTransformTasks( //now transformTask is responsible for compiling this source set into the classes directory sourceSet.compiledBy(transformTask) (tasks.findByName(sourceSet.jarTaskName) as? Jar)?.apply { - setupJarManifest(multiRelease = config.variant.toVariant() == Variant.BOTH) + setupJarManifest(multiRelease = config.jvmVariant.toJvmVariant() == JvmVariant.BOTH) } // test should compile and run against original production binaries if (compilationType == CompilationType.TEST) { @@ -319,9 +427,7 @@ fun Project.configureTransformTasks( classpath = originalMainClassesDirs + sourceSet.compileClasspath - mainSourceSet.output.classesDirs - (this as? KotlinCompile<*>)?.doFirst { - kotlinOptions.addFriendPaths(originalMainClassesDirs) - } + (this as? KotlinCompile<*>)?.setFriendPaths(originalMainClassesDirs) } // todo: fix test runtime classpath for JS? @@ -331,7 +437,7 @@ fun Project.configureTransformTasks( } } -fun String.toVariant(): Variant = enumValueOf(toUpperCase(Locale.US)) +fun String.toJvmVariant(): JvmVariant = enumValueOf(toUpperCase(Locale.US)) fun Project.createJvmTransformTask(compilation: KotlinCompilation<*>): AtomicFUTransformTask = tasks.create( @@ -348,9 +454,6 @@ fun Project.createJsTransformTask(compilation: KotlinCompilation<*>): AtomicFUTr fun Project.createJvmTransformTask(sourceSet: SourceSet): AtomicFUTransformTask = tasks.create(sourceSet.getTaskName("transform", "atomicfuClasses"), AtomicFUTransformTask::class.java) -fun Project.createJsTransformTask(sourceSet: SourceSet): AtomicFUTransformJsTask = - tasks.create(sourceSet.getTaskName("transform", "atomicfuJsFiles"), AtomicFUTransformJsTask::class.java) - fun AtomicFUTransformTask.configureJvmTask( classpath: FileCollection, classesTaskName: String, @@ -363,7 +466,7 @@ fun AtomicFUTransformTask.configureJvmTask( classPath = classpath inputFiles = originalClassesDir outputDir = transformedClassesDir - variant = config.variant + jvmVariant = config.jvmVariant verbose = config.verbose } @@ -395,7 +498,7 @@ class AtomicFUPluginExtension(pluginVersion: String?) { var dependenciesVersion = pluginVersion var transformJvm = true var transformJs = true - var variant: String = "FU" + var jvmVariant: String = "FU" var verbose: Boolean = false } @@ -413,7 +516,8 @@ open class AtomicFUTransformTask : ConventionTask() { lateinit var classPath: FileCollection @Input - var variant = "FU" + var jvmVariant = "FU" + @Input var verbose = false @@ -422,7 +526,7 @@ open class AtomicFUTransformTask : ConventionTask() { val cp = classPath.files.map { it.absolutePath } inputFiles.files.forEach { inputDir -> AtomicFUTransformer(cp, inputDir, outputDir).let { t -> - t.variant = variant.toVariant() + t.jvmVariant = jvmVariant.toJvmVariant() t.verbose = verbose t.transform() } @@ -438,6 +542,7 @@ open class AtomicFUTransformJsTask : ConventionTask() { @OutputDirectory lateinit var outputDir: File + @Input var verbose = false diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/BaseKotlinGradleTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/BaseKotlinGradleTest.kt deleted file mode 100644 index b81a0c9..0000000 --- a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/BaseKotlinGradleTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.plugin.gradle - -import org.gradle.internal.impldep.com.google.common.io.Files -import org.junit.After -import org.junit.Before -import java.io.File - -abstract class BaseKotlinGradleTest { - private lateinit var workingDir: File - - fun project(name: String, suffix: String = "", fn: Project.() -> Unit) { - workingDir = File("build${File.separator}test-$name$suffix").absoluteFile - workingDir.deleteRecursively() - workingDir.mkdirs() - val testResources = File("src/test/resources") - val originalProjectDir = testResources.resolve("projects/$name").apply { checkExists() } - val projectDir = workingDir.resolve(name).apply { mkdirs() } - originalProjectDir.listFiles().forEach { it.copyRecursively(projectDir.resolve(it.name)) } - - // Add an empty setting.gradle - projectDir.resolve("settings.gradle").writeText("// this file is intentionally left empty") - - Project(projectDir = projectDir).fn() - } -}
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/EmptyProjectTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/EmptyProjectTest.kt deleted file mode 100644 index b542f2b..0000000 --- a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/EmptyProjectTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.plugin.gradle - -import org.junit.Test - -class EmptyProjectTest : BaseKotlinGradleTest() { - @Test - fun testEmpty() = project("empty") { - build("build") {} - } -} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/JsProjectTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/JsProjectTest.kt deleted file mode 100644 index 2201199..0000000 --- a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/JsProjectTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.plugin.gradle - -import org.gradle.testkit.runner.TaskOutcome -import org.junit.Test -import java.io.File - -class JsProjectTest : BaseKotlinGradleTest() { - @Test - fun testKotlin2JsPlugin() = project("js-simple") { - val tasksToCheck = arrayOf( - ":compileKotlin2Js", - ":compileTestKotlin2Js", - ":transformAtomicfuJsFiles", - ":transformTestAtomicfuJsFiles" - ) - - build("build") { - checkOutcomes(TaskOutcome.SUCCESS, *tasksToCheck) - - val testCompileClasspathFiles = projectDir.resolve("build/test_compile_classpath.txt") - .readLines().asSequence().flatMap { File(it).walk().filter(File::isFile) }.toHashSet() - - projectDir.resolve("build/classes/kotlin/main/js-simple.js").let { - it.checkExists() - check(it in testCompileClasspathFiles) { "Original '$it' is missing from test compile classpath" } - // todo: check test runtime classpath when js test tasks are supported in plugin - } - - projectDir.resolve("build/classes/atomicfu/main/js-simple.js").let { - it.checkExists() - check(it !in testCompileClasspathFiles) { "Transformed '$it' is present in test compile classpath" } - } - } - - build("build") { - checkOutcomes(TaskOutcome.UP_TO_DATE, *tasksToCheck) - } - } -} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/JvmProjectTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/JvmProjectTest.kt deleted file mode 100644 index e017f05..0000000 --- a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/JvmProjectTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.plugin.gradle - -import org.gradle.testkit.runner.TaskOutcome -import org.junit.Test -import java.io.File - -class JvmProjectTest : BaseKotlinGradleTest() { - @Test - fun testKotlinPlugin() = - project("jvm-simple") { - doSimpleTest() - } - - @Test - fun testKotlinPlatformJvmPlugin() = - project("jvm-simple", "-platform") { - projectDir.resolve("build.gradle").modify { - it.checkedReplace("apply plugin: 'kotlin'", "apply plugin: 'kotlin-platform-jvm'") - } - doSimpleTest() - } - - private fun Project.doSimpleTest() { - val tasksToCheck = arrayOf( - ":compileKotlin", - ":compileTestKotlin", - ":transformAtomicfuClasses", - ":transformTestAtomicfuClasses" - ) - - build("build") { - checkOutcomes(TaskOutcome.SUCCESS, *tasksToCheck) - - val testCompileClasspathFiles = filesFrom("build/test_compile_classpath.txt") - val testRuntimeClasspathFiles = filesFrom("build/test_runtime_classpath.txt") - - projectDir.resolve("build/classes/kotlin/main/IntArithmetic.class").let { - it.checkExists() - check(it in testCompileClasspathFiles) { "Original '$it' is missing from test compile classpath" } - check(it in testRuntimeClasspathFiles) { "Original '$it' is missing from test runtime classpath" } - } - - projectDir.resolve("build/classes/atomicfu/main/IntArithmetic.class").let { - it.checkExists() - check(it !in testCompileClasspathFiles) { "Transformed '$it' is present in test compile classpath" } - check(it !in testRuntimeClasspathFiles) { "Transformed '$it' is present in test runtime classpath" } - } - } - - build("build") { - checkOutcomes(TaskOutcome.UP_TO_DATE, *tasksToCheck) - } - } - - private fun Project.filesFrom(name: String) = projectDir.resolve(name) - .readLines().asSequence().flatMap { listFiles(it) }.toHashSet() - - private fun listFiles(dir: String): Sequence<File> = File(dir).walk().filter { it.isFile } -} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/MppProjectTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/MppProjectTest.kt deleted file mode 100644 index 6dd7fa1..0000000 --- a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/MppProjectTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.plugin.gradle - -import org.gradle.testkit.runner.TaskOutcome -import org.junit.Test -import java.io.File - -class MppProjectTest : BaseKotlinGradleTest() { - @Test - fun testKotlinMultiplatformPlugin() = project("mpp-simple") { - val tasksToCheck = arrayOf( - ":compileKotlinJvm", - ":compileTestKotlinJvm", - ":transformJvmMainAtomicfu", - ":transformJvmTestAtomicfu", - ":compileKotlinJs", - ":transformJsMainAtomicfu" - ) - - build("build") { - checkOutcomes(TaskOutcome.SUCCESS, *tasksToCheck) - - fun checkPlatform(platform: String, fileInMainName: String) { - val isJs = platform == "js" - val testCompileClasspathFiles = projectDir.resolve("build/classpath/$platform/test_compile.txt") - .readLines().asSequence().flatMapTo(HashSet()) { File(it).walk().filter(File::isFile) } - val testRuntimeClasspathFiles = if (isJs) emptySet<File>() else projectDir.resolve("build/classpath/$platform/test_runtime.txt") - .readLines().asSequence().flatMapTo(HashSet()) { File(it).walk().filter(File::isFile) } - - projectDir.resolve("build/classes/kotlin/$platform/main/$fileInMainName").let { - it.checkExists() - check(it in testCompileClasspathFiles) { "Original '$it' is missing from $platform test compile classpath" } - if (!isJs) check(it in testRuntimeClasspathFiles) { "Original '$it' is missing from $platform test runtime classpath" } - } - - projectDir.resolve("build/classes/atomicfu/jvm/main/IntArithmetic.class").let { - it.checkExists() - check(it !in testCompileClasspathFiles) { "Transformed '$it' is present in $platform test compile classpath" } - if (!isJs) check(it !in testRuntimeClasspathFiles) { "Transformed '$it' is present in $platform test runtime classpath" } - } - - } - - checkPlatform("jvm", "IntArithmetic.class") - checkPlatform("js", "mpp-simple.js") - } - - build("build") { - checkOutcomes(TaskOutcome.UP_TO_DATE, *tasksToCheck) - } - } -} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/Project.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/Project.kt deleted file mode 100644 index f3f49e1..0000000 --- a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/Project.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.plugin.gradle - -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.GradleRunner -import java.io.File - -class Project(val projectDir: File) { - init { - projectDir.resolve("build.gradle").modify { - buildScript + "\n\n" + it - } - } - - private var isDebug = false - private var printStdout = false - - @Deprecated("Should be used for debug only!") - @Suppress("unused") - fun debug() { - isDebug = true - } - - /** - * Redirects Gradle runner output to stdout. Useful for debugging. - */ - @Deprecated("Should be used for debug only!") - @Suppress("unused") - fun printStdout() { - printStdout = true - } - - fun gradle(vararg tasks: String): GradleRunner = - GradleRunner.create() - .withDebug(isDebug) - .withProjectDir(projectDir) - .withArguments(*(defaultArguments() + tasks)) - .run { - if (printStdout) { - forwardStdOutput(System.out.bufferedWriter()) - } else { - this - } - } - - fun build(vararg tasks: String, fn: BuildResult.() -> Unit = {}) { - val gradle = gradle(*tasks) - val buildResult = gradle.build() - buildResult.fn() - } - - @Suppress("unused") - fun buildAndFail(vararg tasks: String, fn: BuildResult.() -> Unit = {}) { - val gradle = gradle(*tasks) - val buildResult = gradle.buildAndFail() - buildResult.fn() - } - - private fun defaultArguments(): Array<String> = - arrayOf("--stacktrace") - - companion object { - private fun readFileList(fileName: String): String { - val resource = Project::class.java.classLoader.getResource(fileName) - ?: throw IllegalStateException("Could not find resource '$fileName'") - val files = File(resource.toURI()) - .readLines() - .map { File(it).absolutePath.replace("\\", "\\\\") } // escape backslashes in Windows paths - return files.joinToString(", ") { "'$it'" } - } - - private val buildScript = run { - """ - buildscript { - dependencies { - classpath files(${readFileList("plugin-classpath.txt")}) - } - } - - repositories { - jcenter() - mavenCentral() - maven { url 'https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev' } - maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } - maven { url 'https://dl.bintray.com/kotlin/kotlin-dev' } - maven { url 'https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev' } - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } - } - - def atomicfuJvm = files(${readFileList("atomicfu-jvm.txt")}) - def atomicfuJs = files(${readFileList("atomicfu-js.txt")}) - def atomicfuMetadata = files(${readFileList("atomicfu-metadata.txt")}) - """.trimIndent() - } - } -} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/Assert.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/Assert.kt new file mode 100644 index 0000000..f55e38a --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/Assert.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2022 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.atomicfu.plugin.gradle.internal + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome +import kotlin.test.assertEquals + +/** + * Helper `fun` for asserting a [TaskOutcome] to be equal to [TaskOutcome.SUCCESS] + */ +internal fun BuildResult.assertTaskSuccess(task: String) { + assertTaskOutcome(TaskOutcome.SUCCESS, task) +} + +/** + * Helper `fun` for asserting a [TaskOutcome] to be equal to [TaskOutcome.FAILED] + */ +internal fun BuildResult.assertTaskFailure(task: String) { + assertTaskOutcome(TaskOutcome.FAILED, task) +} + +internal fun BuildResult.assertTaskUpToDate(task: String) { + assertTaskOutcome(TaskOutcome.UP_TO_DATE, task) +} + +private fun BuildResult.assertTaskOutcome(taskOutcome: TaskOutcome, taskName: String) { + assertEquals(taskOutcome, task(taskName)?.outcome) +} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/TestDsl.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/TestDsl.kt new file mode 100644 index 0000000..2541b41 --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/TestDsl.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2016-2022 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.atomicfu.plugin.gradle.internal + +import kotlinx.atomicfu.plugin.gradle.test.* +import org.gradle.testkit.runner.* +import java.io.* + +internal fun BaseKotlinGradleTest.test(fn: BaseKotlinScope.() -> Unit): GradleRunner { + val baseKotlinScope = BaseKotlinScope() + fn(baseKotlinScope) + + baseKotlinScope.files.forEach { scope -> + val fileWriteTo = rootProjectDir.resolve(scope.filePath) + .apply { + parentFile.mkdirs() + createNewFile() + } + + scope.files.forEach { + val fileContent = readFileList(it) + fileWriteTo.appendText(fileContent) + } + } + + return GradleRunner.create() + .withProjectDir(rootProjectDir) + .withArguments(baseKotlinScope.runner.arguments) + .withPluginClasspath() + .addPluginTestRuntimeClasspath() +} + +/** + * same as [file][FileContainer.file], but prepends "src/${sourceSet}/kotlin" before given `classFileName` + */ +internal fun FileContainer.kotlin(classFileName: String, sourceSet: String = "main", fn: AppendableScope.() -> Unit) { + require(classFileName.endsWith(".kt")) { + "ClassFileName must end with '.kt'" + } + + val fileName = "src/${sourceSet}/kotlin/$classFileName" + file(fileName, fn) +} + +/** + * Shortcut for creating a `build.gradle.kts` by using [file][FileContainer.file] + */ +internal fun FileContainer.buildGradleKts(fn: AppendableScope.() -> Unit) { + val fileName = "build.gradle.kts" + file(fileName, fn) +} + +/** + * Shortcut for creating a `settings.gradle.kts` by using [file][FileContainer.file] + */ +internal fun FileContainer.settingsGradleKts(fn: AppendableScope.() -> Unit) { + val fileName = "settings.gradle.kts" + file(fileName, fn) +} + +/** + * Shortcut for creating a `gradle.properties` by using [file][FileContainer.file] + */ +internal fun FileContainer.gradleProperties(fn: AppendableScope.() -> Unit) { + val fileName = "gradle.properties" + file(fileName, fn) +} + +/** + * Declares a directory with the given [dirName] inside the current container. + * All calls creating files within this scope will create the files nested in this directory. + * + * Note that it is valid to call this method multiple times at the same level with the same [dirName]. + * Files declared within 2 independent calls to [dir] will be added to the same directory. + */ +internal fun FileContainer.dir(dirName: String, fn: DirectoryScope.() -> Unit) { + DirectoryScope(dirName, this).fn() +} + +internal fun BaseKotlinScope.runner(fn: Runner.() -> Unit) { + val runner = Runner() + fn(runner) + + this.runner = runner +} + +internal fun AppendableScope.resolve(fileName: String) { + this.files.add(fileName) +} + +internal interface FileContainer { + fun file(fileName: String, fn: AppendableScope.() -> Unit) +} + +internal class BaseKotlinScope : FileContainer { + var files: MutableList<AppendableScope> = mutableListOf() + var runner: Runner = Runner() + + override fun file(fileName: String, fn: AppendableScope.() -> Unit) { + val appendableScope = AppendableScope(fileName) + fn(appendableScope) + files.add(appendableScope) + } +} + +internal class DirectoryScope( + val dirPath: String, + val parent: FileContainer +): FileContainer { + + override fun file(fileName: String, fn: AppendableScope.() -> Unit) { + parent.file("$dirPath/$fileName", fn) + } +} + +internal class AppendableScope(val filePath: String) { + val files: MutableList<String> = mutableListOf() +} + +internal class Runner { + val arguments: MutableList<String> = mutableListOf() +} + +internal fun readFileList(fileName: String): String = + getFile(fileName).readText() + +internal fun getFile(fileName: String): File { + val resource = BaseKotlinGradleTest::class.java.classLoader.getResource(fileName) + ?: throw IllegalStateException("Could not find resource '$fileName'") + return File(resource.toURI()) +} + +internal fun GradleRunner.addPluginTestRuntimeClasspath() = apply { + val pluginClasspath = getFile("plugin-classpath.txt").readLines().map { File(it) } + withPluginClasspath(pluginClasspath) +} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/utils.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/utils.kt new file mode 100644 index 0000000..49856f2 --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/internal/utils.kt @@ -0,0 +1,13 @@ +package kotlinx.atomicfu.plugin.gradle.internal + +import java.io.* +import kotlin.test.* + +fun File.checkExists() { + assertTrue(exists(), "File does not exist: $canonicalPath") +} + +fun File.filesFrom(relative: String) = resolve(relative) + .readLines().asSequence().flatMap { listFiles(it) }.toHashSet() + +fun listFiles(dir: String): Sequence<File> = File(dir).walk().filter { it.isFile }
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/BaseKotlinGradleTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/BaseKotlinGradleTest.kt new file mode 100644 index 0000000..e9ff7bb --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/BaseKotlinGradleTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.atomicfu.plugin.gradle.test + +import kotlinx.atomicfu.plugin.gradle.internal.* +import org.objectweb.asm.* +import java.io.File +import kotlin.test.* + +abstract class BaseKotlinGradleTest(private val projectName: String) { + internal val rootProjectDir: File + private val ATOMIC_FU_REF = "Lkotlinx/atomicfu/".toByteArray() + private val KOTLIN_METADATA_DESC = "Lkotlin/Metadata;" + + init { + rootProjectDir = File("build${File.separator}test-$projectName").absoluteFile + rootProjectDir.deleteRecursively() + rootProjectDir.mkdirs() + } + + internal abstract fun BaseKotlinScope.createProject() + + val runner = test { + createProject() + runner { + arguments.add(":build") + } + } + + fun checkTaskOutcomes(executedTasks: List<String>, excludedTasks: List<String>) { + runner.build().apply { + val tasks = tasks.map { it.path } + excludedTasks.forEach { + check(it !in tasks) { "Post-compilation transformation task $it was added in the compiler plugin mode" } + } + executedTasks.forEach { + assertTaskSuccess(it) + } + } + // check that task outcomes are cached for the second build + runner.build().apply { + executedTasks.forEach { + assertTaskUpToDate(it) + } + } + } + + fun checkJvmCompilationClasspath(originalClassFile: String, transformedClassFile: String) { + // check that test compile and runtime classpath does not contain original non-transformed classes + val testCompileClasspathFiles = rootProjectDir.filesFrom("build/test_compile_jvm_classpath.txt") + val testRuntimeClasspathFiles = rootProjectDir.filesFrom("build/test_runtime_jvm_classpath.txt") + + rootProjectDir.resolve(transformedClassFile).let { + it.checkExists() + check(it in testCompileClasspathFiles) { "Transformed '$it' is missing from test compile classpath" } + check(it in testRuntimeClasspathFiles) { "Transformed '$it' is missing from test runtime classpath" } + } + + rootProjectDir.resolve(originalClassFile).let { + it.checkExists() + check(it !in testCompileClasspathFiles) { "Original '$it' is present in test compile classpath" } + check(it !in testRuntimeClasspathFiles) { "Original '$it' is present in test runtime classpath" } + } + } + + fun checkJsCompilationClasspath() { + // check that test compilation depends on transformed main sources + val testCompileClasspathFiles = rootProjectDir.filesFrom("build/test_compile_js_classpath.txt") + + rootProjectDir.resolve("build/classes/atomicfu/js/main/$projectName.js").let { + it.checkExists() + check(it in testCompileClasspathFiles) { "Transformed '$it' is missing from test compile classpath" } + } + } + + fun checkBytecode(classFilePath: String) { + rootProjectDir.resolve(classFilePath).let { + it.checkExists() + assertFalse(it.readBytes().findAtomicfuRef(), "Found 'Lkotlinx/atomicfu/' reference in $it" ) + } + } + + private fun ByteArray.findAtomicfuRef(): Boolean { + val bytes = this.eraseMetadata() + loop@for (i in 0 until bytes.size - ATOMIC_FU_REF.size) { + for (j in 0 until ATOMIC_FU_REF.size) { + if (bytes[i + j] != ATOMIC_FU_REF[j]) continue@loop + } + return true + } + return false + } + + // 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() + } +} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/JsProjectTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/JsProjectTest.kt new file mode 100644 index 0000000..0b34955 --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/JsProjectTest.kt @@ -0,0 +1,49 @@ +package kotlinx.atomicfu.plugin.gradle.test + +import kotlinx.atomicfu.plugin.gradle.internal.* +import kotlinx.atomicfu.plugin.gradle.internal.BaseKotlinScope +import org.junit.Test + +/** + * Test that ensures correctness of `atomicfu-gradle-plugin` application to the JS project: + * - post-compilation js transformation tasks are created + * (legacy transformation is tested here, compiler plugin is not applied). + * - original non-transformed classes are not left in compile/runtime classpath. + */ +class JsLegacyTransformationTest : BaseKotlinGradleTest("js-simple") { + + override fun BaseKotlinScope.createProject() { + buildGradleKts { + resolve("projects/js-simple/js-simple.gradle.kts") + } + settingsGradleKts { + resolve("projects/js-simple/settings.gradle.kts") + } + dir("src/main/kotlin") {} + kotlin("IntArithmetic.kt", "main") { + resolve("projects/js-simple/src/main/kotlin/IntArithmetic.kt") + } + dir("src/test/kotlin") {} + kotlin("ArithmeticTest.kt", "test") { + resolve("projects/js-simple/src/test/kotlin/ArithmeticTest.kt") + } + } + + @Test + fun testPluginApplication() = + checkTaskOutcomes( + executedTasks = listOf( + ":compileKotlinJs", + ":transformJsMainAtomicfu", + ":compileTestKotlinJs", + ":transformJsTestAtomicfu" + ), + excludedTasks = emptyList() + ) + + @Test + fun testClasspath() { + runner.build() + checkJsCompilationClasspath() + } +} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/JvmProjectTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/JvmProjectTest.kt new file mode 100644 index 0000000..2545e0e --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/JvmProjectTest.kt @@ -0,0 +1,108 @@ +package kotlinx.atomicfu.plugin.gradle.test + +import kotlinx.atomicfu.plugin.gradle.internal.* +import kotlinx.atomicfu.plugin.gradle.internal.BaseKotlinScope +import org.junit.Test + +/** + * Test that ensures correctness of `atomicfu-gradle-plugin` application to the JVM project: + * - post-compilation bytecode transformation tasks are created + * (legacy transformation is tested here, compiler plugin is not applied). + * - original non-transformed classes are not left in compile/runtime classpath. + * - no `kotlinx/atomicfu` references are left in the transformed bytecode. + */ +class JvmLegacyTransformationTest : BaseKotlinGradleTest("jvm-simple") { + + override fun BaseKotlinScope.createProject() { + buildGradleKts { + resolve("projects/jvm-simple/jvm-simple.gradle.kts") + } + settingsGradleKts { + resolve("projects/jvm-simple/settings.gradle.kts") + } + dir("src/main/kotlin") {} + kotlin("IntArithmetic.kt", "main") { + resolve("projects/jvm-simple/src/main/kotlin/IntArithmetic.kt") + } + dir("src/test/kotlin") {} + kotlin("ArithmeticTest.kt", "test") { + resolve("projects/jvm-simple/src/test/kotlin/ArithmeticTest.kt") + } + } + + @Test + fun testPluginApplication() = + checkTaskOutcomes( + executedTasks = listOf( + ":compileKotlin", + ":transformAtomicfuClasses", + ":compileTestKotlin", + ":transformTestAtomicfuClasses" + ), + excludedTasks = emptyList() + ) + + @Test + fun testClasspath() { + runner.build() + checkJvmCompilationClasspath( + originalClassFile = "build/classes/kotlin/main/IntArithmetic.class", + transformedClassFile = "build/classes/atomicfu/main/IntArithmetic.class" + ) + } + + @Test + fun testAtomicfuReferences() { + runner.build() + checkBytecode("build/classes/atomicfu/main/IntArithmetic.class") + } +} + +/** + * Test that ensures correctness of `atomicfu-gradle-plugin` application to the JVM project, + * - JVM IR compiler plugin transformation (kotlinx.atomicfu.enableJvmIrTransformation=true) + * - no post-compilation bytecode transforming tasks created + * - no `kotlinx/atomicfu` references are left in the resulting bytecode after IR transformation. + */ +class JvmIrTransformationTest : BaseKotlinGradleTest("jvm-simple") { + + override fun BaseKotlinScope.createProject() { + buildGradleKts { + resolve("projects/jvm-simple/jvm-simple.gradle.kts") + } + settingsGradleKts { + resolve("projects/jvm-simple/settings.gradle.kts") + } + // set kotlinx.atomicfu.enableJvmIrTransformation=true to apply compiler plugin + gradleProperties { + resolve("projects/jvm-simple/gradle.properties") + } + dir("src/main/kotlin") {} + kotlin("IntArithmetic.kt", "main") { + resolve("projects/jvm-simple/src/main/kotlin/IntArithmetic.kt") + } + dir("src/test/kotlin") {} + kotlin("ArithmeticTest.kt", "test") { + resolve("projects/jvm-simple/src/test/kotlin/ArithmeticTest.kt") + } + } + + @Test + fun testPluginApplication() = + checkTaskOutcomes( + executedTasks = listOf( + ":compileKotlin", + ":compileTestKotlin" + ), + excludedTasks = listOf( + ":transformAtomicfuClasses", + ":transformTestAtomicfuClasses" + ) + ) + + @Test + fun testAtomicfuReferences() { + runner.build() + checkBytecode("build/classes/kotlin/main/IntArithmetic.class") + } +} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/MppProjectTest.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/MppProjectTest.kt new file mode 100644 index 0000000..e95b091 --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/test/MppProjectTest.kt @@ -0,0 +1,219 @@ +package kotlinx.atomicfu.plugin.gradle.test + +import kotlinx.atomicfu.plugin.gradle.internal.* +import org.junit.* + +/** + * Test that ensures correctness of `atomicfu-gradle-plugin` application to the MPP project: + * - post-compilation bytecode transformation tasks are created + * (legacy transformation is tested here, compiler plugin is not applied). + * - original non-transformed classes are not left in compile/runtime classpath. + * - no `kotlinx/atomicfu` references are left in the transformed bytecode. + */ +class MppLegacyTransformationTest : BaseKotlinGradleTest("mpp-simple") { + + override fun BaseKotlinScope.createProject() { + buildGradleKts { + resolve("projects/mpp-simple/mpp-simple.gradle.kts") + } + settingsGradleKts { + resolve("projects/mpp-simple/settings.gradle.kts") + } + dir("src/commonMain/kotlin") {} + kotlin("IntArithmetic.kt", "commonMain") { + resolve("projects/mpp-simple/src/commonMain/kotlin/IntArithmetic.kt") + } + dir("src/commonTest/kotlin") {} + kotlin("ArithmeticTest.kt", "commonTest") { + resolve("projects/mpp-simple/src/commonTest/kotlin/ArithmeticTest.kt") + } + } + + @Test + fun testPluginApplication() = + checkTaskOutcomes( + executedTasks = listOf( + ":compileKotlinJvm", + ":compileTestKotlinJvm", + ":transformJvmMainAtomicfu", + ":transformJvmTestAtomicfu", + ":compileKotlinJs", + ":transformJsMainAtomicfu" + ), + excludedTasks = emptyList() + ) + + @Test + fun testClasspath() { + runner.build() + checkJvmCompilationClasspath( + originalClassFile = "build/classes/kotlin/jvm/main/IntArithmetic.class", + transformedClassFile = "build/classes/atomicfu/jvm/main/IntArithmetic.class" + ) + checkJsCompilationClasspath() + } + + @Test + fun testAtomicfuReferences() { + runner.build() + checkBytecode("build/classes/atomicfu/jvm/main/IntArithmetic.class") + } +} + +/** + * Test that ensures correctness of `atomicfu-gradle-plugin` application to the MPP project, + * - JVM IR compiler plugin transformation (kotlinx.atomicfu.enableJvmIrTransformation=true) + * - no post-compilation bytecode transformation tasks are created + * - post-compilation js file transformation task created (as only JVM IR transformation applied, js is transformed in legacy mode) + * - no `kotlinx/atomicfu` references are left in the transformed bytecode. + */ +class MppJvmIrTransformationTest : BaseKotlinGradleTest("mpp-simple") { + + override fun BaseKotlinScope.createProject() { + buildGradleKts { + resolve("projects/mpp-simple/mpp-simple.gradle.kts") + } + settingsGradleKts { + resolve("projects/mpp-simple/settings.gradle.kts") + } + gradleProperties { + resolve("projects/mpp-simple/gradle.properties_jvm") + } + dir("src/commonMain/kotlin") {} + kotlin("IntArithmetic.kt", "commonMain") { + resolve("projects/mpp-simple/src/commonMain/kotlin/IntArithmetic.kt") + } + dir("src/commonTest/kotlin") {} + kotlin("ArithmeticTest.kt", "commonTest") { + resolve("projects/mpp-simple/src/commonTest/kotlin/ArithmeticTest.kt") + } + } + + @Test + fun testPluginApplication() = + checkTaskOutcomes( + executedTasks = listOf( + ":compileKotlinJvm", + ":compileTestKotlinJvm", + ":compileKotlinJs", + // legacy JS transformation + ":transformJsMainAtomicfu", + ":transformJsTestAtomicfu" + ), + excludedTasks = listOf( + ":transformJvmMainAtomicfu", + ":transformJvmTestAtomicfu" + ) + ) + + @Test + fun testAtomicfuReferences() { + runner.build() + checkBytecode("build/classes/kotlin/jvm/main/IntArithmetic.class") + } +} + +/** + * Test that ensures correctness of `atomicfu-gradle-plugin` application to the MPP project, + * - JS IR compiler plugin transformation (kotlinx.atomicfu.enableJsIrTransformation=true) + * - post-compilation bytecode transformation tasks are created (only JS IR transformation is applied, jvm is transformed in legacy mode) + * - no post-compilation js file transformation tasks are created + * - no `kotlinx/atomicfu` references are left in the transformed bytecode. + */ +class MppJsIrTransformationTest : BaseKotlinGradleTest("mpp-simple") { + + override fun BaseKotlinScope.createProject() { + buildGradleKts { + resolve("projects/mpp-simple/mpp-simple.gradle.kts") + } + settingsGradleKts { + resolve("projects/mpp-simple/settings.gradle.kts") + } + gradleProperties { + resolve("projects/mpp-simple/gradle.properties_js") + } + dir("src/commonMain/kotlin") {} + kotlin("IntArithmetic.kt", "commonMain") { + resolve("projects/mpp-simple/src/commonMain/kotlin/IntArithmetic.kt") + } + dir("src/commonTest/kotlin") {} + kotlin("ArithmeticTest.kt", "commonTest") { + resolve("projects/mpp-simple/src/commonTest/kotlin/ArithmeticTest.kt") + } + } + + @Test + fun testPluginApplication() = + checkTaskOutcomes( + executedTasks = listOf( + ":compileKotlinJvm", + ":compileTestKotlinJvm", + ":compileKotlinJs", + // legacy JVM transformation + ":transformJvmMainAtomicfu", + ":transformJvmTestAtomicfu" + ), + excludedTasks = listOf( + ":transformJsMainAtomicfu", + ":transformJsTestAtomicfu" + ) + ) + + @Test + fun testAtomicfuReferences() { + runner.build() + checkBytecode("build/classes/atomicfu/jvm/main/IntArithmetic.class") + } +} + +/** + * Test that ensures correctness of `atomicfu-gradle-plugin` application to the MPP project, + * - JS IR and JVM IR compiler plugin transformation + * - no post-compilation bytecode transformation tasks are created (only JS IR transformation is applied, jvm is transformed in legacy mode) + * - no post-compilation js file transformation tasks are created + * - no `kotlinx/atomicfu` references are left in the transformed bytecode. + */ +class MppBothIrTransformationTest : BaseKotlinGradleTest("mpp-simple") { + + override fun BaseKotlinScope.createProject() { + buildGradleKts { + resolve("projects/mpp-simple/mpp-simple.gradle.kts") + } + settingsGradleKts { + resolve("projects/mpp-simple/settings.gradle.kts") + } + gradleProperties { + resolve("projects/mpp-simple/gradle.properties_both") + } + dir("src/commonMain/kotlin") {} + kotlin("IntArithmetic.kt", "commonMain") { + resolve("projects/mpp-simple/src/commonMain/kotlin/IntArithmetic.kt") + } + dir("src/commonTest/kotlin") {} + kotlin("ArithmeticTest.kt", "commonTest") { + resolve("projects/mpp-simple/src/commonTest/kotlin/ArithmeticTest.kt") + } + } + + @Test + fun testPluginApplication() = + checkTaskOutcomes( + executedTasks = listOf( + ":compileKotlinJvm", + ":compileTestKotlinJvm", + ":compileKotlinJs" + ), + excludedTasks = listOf( + ":transformJvmMainAtomicfu", + ":transformJvmTestAtomicfu", + ":transformJsMainAtomicfu", + ":transformJsTestAtomicfu" + ) + ) + + @Test + fun testAtomicfuReferences() { + runner.build() + checkBytecode("build/classes/kotlin/jvm/main/IntArithmetic.class") + } +} diff --git a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/utils.kt b/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/utils.kt deleted file mode 100644 index 2a8d0f7..0000000 --- a/atomicfu-gradle-plugin/src/test/kotlin/kotlinx/atomicfu/plugin/gradle/utils.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.plugin.gradle - -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.TaskOutcome -import java.io.File -import kotlin.test.assertTrue - -fun BuildResult.checkOutcomes(expected: TaskOutcome, vararg tasks: String) { - val unexpectedOutcomes = tasks - .map { it to task(it)?.outcome } - .filter { (_, outcome) -> outcome != expected } - if (unexpectedOutcomes.isNotEmpty()) { - throw AssertionError("Unexpected outcomes for tasks." + - "\nExpected: $expected." + - "\nGot:" + - "\n${unexpectedOutcomes.joinToString("\n") { (task, outcome) -> "* $task -> $outcome" }}") - - } -} - -fun File.checkExists() { - assertTrue(exists(), "File does not exist: $canonicalPath") -} - -fun File.modify(fn: (String) -> String) { - writeText(fn(readText())) -} - -fun String.checkedReplace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String { - check(contains(oldValue, ignoreCase)) { "String must contain '$oldValue'" } - return replace(oldValue, newValue, ignoreCase) -}
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/empty/build.gradle b/atomicfu-gradle-plugin/src/test/resources/projects/empty/build.gradle deleted file mode 100644 index f4add41..0000000 --- a/atomicfu-gradle-plugin/src/test/resources/projects/empty/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -apply plugin: 'kotlinx-atomicfu' -apply plugin: 'base' - -repositories { - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } -}
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/build.gradle b/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/build.gradle deleted file mode 100644 index 31dbec6..0000000 --- a/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -apply plugin: 'kotlinx-atomicfu' -apply plugin: 'kotlin2js' - -dependencies { - compileOnly atomicfuJs - testRuntime atomicfuJs - - compile 'org.jetbrains.kotlin:kotlin-stdlib-js' - testCompile 'org.jetbrains.kotlin:kotlin-test-js' -} - -compileTestKotlin2Js.doLast { - file("$buildDir/test_compile_classpath.txt").text = classpath.join("\n") -}
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/js-simple.gradle.kts b/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/js-simple.gradle.kts new file mode 100644 index 0000000..37a41e5 --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/js-simple.gradle.kts @@ -0,0 +1,38 @@ +import kotlinx.atomicfu.plugin.gradle.* + +buildscript { + dependencies { + classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.17.0") + } +} + +plugins { + kotlin("js") +} + +apply(plugin = "kotlinx-atomicfu") + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib-js")) + implementation(kotlin("test-junit")) + implementation("org.jetbrains.kotlin:kotlin-test-js") +} + +kotlin { + js { + nodejs() + } + + tasks.named("compileTestKotlinJs") { + doLast { + file("$buildDir/test_compile_js_classpath.txt").writeText( + target.compilations["test"].compileDependencyFiles.joinToString("\n") + ) + } + } +} diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/settings.gradle.kts b/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/settings.gradle.kts new file mode 100644 index 0000000..bd39e74 --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/js-simple/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "js-simple"
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/build.gradle b/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/build.gradle deleted file mode 100644 index 7e21215..0000000 --- a/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -apply plugin: 'kotlinx-atomicfu' -apply plugin: 'kotlin' - -// This flag is enabled to be able using JVM IR compiled dependencies (when build is ran with -Penable_jvm_ir) -kotlin.target.compilations.all { - kotlinOptions.freeCompilerArgs += '-Xallow-jvm-ir-dependencies' -} - -dependencies { - compileOnly atomicfuJvm - testRuntime atomicfuJvm - - compile 'org.jetbrains.kotlin:kotlin-stdlib' - - testCompile 'org.jetbrains.kotlin:kotlin-test' - testCompile 'org.jetbrains.kotlin:kotlin-test-junit' - testCompile 'junit:junit:4.12' -} - -compileTestKotlin.doLast { - file("$buildDir/test_compile_classpath.txt").text = classpath.join("\n") -} - -test.doLast { - file("$buildDir/test_runtime_classpath.txt").text = classpath.join("\n") -}
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/gradle.properties b/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/gradle.properties new file mode 100644 index 0000000..fa37a2c --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/gradle.properties @@ -0,0 +1 @@ +kotlinx.atomicfu.enableJvmIrTransformation=true diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/jvm-simple.gradle.kts b/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/jvm-simple.gradle.kts new file mode 100644 index 0000000..db644ef --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/jvm-simple.gradle.kts @@ -0,0 +1,41 @@ +import org.gradle.api.tasks.compile.* +import org.jetbrains.kotlin.gradle.plugin.* + +buildscript { + dependencies { + classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.17.0") + } +} + +plugins { + kotlin("jvm") +} + +apply(plugin = "kotlinx-atomicfu") + +repositories { + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-junit")) +} + +kotlin { + tasks.compileTestKotlin { + doLast { + file("$buildDir/test_compile_jvm_classpath.txt").writeText( + target.compilations["test"].compileDependencyFiles.joinToString("\n") + ) + } + } + + tasks.test { + doLast { + file("$buildDir/test_runtime_jvm_classpath.txt").writeText( + (target.compilations["test"] as KotlinCompilationToRunnableFiles<*>).runtimeDependencyFiles.joinToString("\n") + ) + } + } +} diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/settings.gradle.kts b/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/settings.gradle.kts new file mode 100644 index 0000000..2f5327f --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/jvm-simple/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "jvm-simple"
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/build.gradle b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/build.gradle deleted file mode 100644 index fc95366..0000000 --- a/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/build.gradle +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -apply plugin: 'kotlinx-atomicfu' -apply plugin: 'kotlin-multiplatform' - -kotlin { - // This flag is enabled to be able using JVM IR compiled dependencies (when build is ran with -Penable_jvm_ir) - jvm() { - compilations.all { - kotlinOptions.freeCompilerArgs += '-Xallow-jvm-ir-dependencies' - } - } - js() - - sourceSets { - commonMain.dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib-common' - compileOnly atomicfuMetadata - - } - commonTest.dependencies { - implementation 'org.jetbrains.kotlin:kotlin-test-common' - implementation 'org.jetbrains.kotlin:kotlin-test-annotations-common' - runtimeOnly atomicfuMetadata - - } - jsMain.dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib-js' - compileOnly atomicfuJs - - } - jsTest.dependencies { - implementation 'org.jetbrains.kotlin:kotlin-test-js' - runtimeOnly atomicfuJs - } - jvmMain.dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib' - compileOnly atomicfuJvm - } - jvmTest.dependencies { - implementation 'org.jetbrains.kotlin:kotlin-test' - implementation 'org.jetbrains.kotlin:kotlin-test-junit' - implementation "junit:junit:4.12" - runtimeOnly atomicfuJvm - } - } -} - -def File classpathFile(String platform, String fileName) { - def dir = file("$buildDir/classpath/$platform") - dir.mkdirs() - return file("$dir/$fileName") -} - - -compileTestKotlinJvm.doLast { - classpathFile("jvm", "test_compile.txt").text = classpath.files.join("\n") -} - -jvmTest.doLast { - classpathFile("jvm", "test_runtime.txt").text = classpath.files.join("\n") -} - - -compileTestKotlinJs.doLast { - classpathFile("js", "test_compile.txt").text = classpath.files.join("\n") -} - -jsTest.dependsOn(":compileTestKotlinJs") -jsTest.dependsOn(":transformJsTestAtomicfu") -check.dependsOn(":jsTest")
\ No newline at end of file diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_both b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_both new file mode 100644 index 0000000..5d28ccd --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_both @@ -0,0 +1,3 @@ +kotlin.js.compiler=ir +kotlinx.atomicfu.enableJvmIrTransformation=true +kotlinx.atomicfu.enableJsIrTransformation=true diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_js b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_js new file mode 100644 index 0000000..b57875b --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_js @@ -0,0 +1,2 @@ +kotlin.js.compiler=ir +kotlinx.atomicfu.enableJsIrTransformation=true diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_jvm b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_jvm new file mode 100644 index 0000000..fa37a2c --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/gradle.properties_jvm @@ -0,0 +1 @@ +kotlinx.atomicfu.enableJvmIrTransformation=true diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/mpp-simple.gradle.kts b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/mpp-simple.gradle.kts new file mode 100644 index 0000000..ed15d3d --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/mpp-simple.gradle.kts @@ -0,0 +1,90 @@ +import org.jetbrains.kotlin.gradle.plugin.* + +buildscript { + dependencies { + classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.17.0") + } +} + +plugins { + kotlin("multiplatform") +} + +apply(plugin = "kotlinx-atomicfu") + +repositories { + mavenCentral() +} + +kotlin { + targets { + jvm { + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + testRuns["test"].executionTask.configure { + useJUnit() + } + } + js { + nodejs() + } + } + sourceSets { + val commonMain by getting { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-common") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmMain by getting { + dependencies { + implementation(kotlin("stdlib")) + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + val jsMain by getting { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-js") + } + } + val jsTest by getting { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-test-js") + } + } + } + + tasks.named("compileTestKotlinJvm") { + doLast { + file("$buildDir/test_compile_jvm_classpath.txt").writeText( + targets["jvm"].compilations["test"].compileDependencyFiles.joinToString("\n") + ) + } + } + + tasks.named("jvmTest") { + doLast { + file("$buildDir/test_runtime_jvm_classpath.txt").writeText( + (targets["jvm"].compilations["test"] as KotlinCompilationToRunnableFiles).runtimeDependencyFiles.joinToString("\n") + ) + } + } + + tasks.named("compileTestKotlinJs") { + doLast { + file("$buildDir/test_compile_js_classpath.txt").writeText( + targets["js"].compilations["test"].compileDependencyFiles.joinToString("\n") + ) + } + } +} diff --git a/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/settings.gradle.kts b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/settings.gradle.kts new file mode 100644 index 0000000..5a4e5ab --- /dev/null +++ b/atomicfu-gradle-plugin/src/test/resources/projects/mpp-simple/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "mpp-simple"
\ No newline at end of file diff --git a/atomicfu-maven-plugin/build.gradle b/atomicfu-maven-plugin/build.gradle index 8929be9..a165769 100644 --- a/atomicfu-maven-plugin/build.gradle +++ b/atomicfu-maven-plugin/build.gradle @@ -18,7 +18,7 @@ dependencies { } def pomFile = file("$buildDir/pom.xml") -def outputDir = compileKotlin.destinationDir +def outputDir = compileKotlin.destinationDirectory def buildSnapshots = rootProject.properties['build_snapshot_train'] != null evaluationDependsOn(':atomicfu-transformer') @@ -37,32 +37,17 @@ task generatePomFile(dependsOn: [compileKotlin, ':atomicfu-transformer:publishTo asNode().with { appendNode('build').with { appendNode('directory', buildDir) - appendNode('outputDirectory', outputDir) + appendNode('outputDirectory', outputDir.get().getAsFile()) } appendNode('properties').with { appendNode('project.build.sourceEncoding', 'UTF-8') } appendNode('repositories').with { appendNode('repository').with { - appendNode('id', 'kotlin-eap') - appendNode('url', 'https://kotlin.bintray.com/kotlin-eap') - } - - appendNode('repository').with { - appendNode('id', 'kotlin-dev') - appendNode('url', 'https://kotlin.bintray.com/kotlin-dev') - } - - appendNode('repository').with { appendNode('id', 'dev') appendNode('url', 'https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev') } - appendNode('repository').with { - appendNode('id', 'kotlinx') - appendNode('url', 'https://kotlin.bintray.com/kotlinx') - } - if (buildSnapshots) { appendNode('repository').with { appendNode('id', 'kotlin-snapshots') @@ -84,7 +69,7 @@ String mavenRepoLocal = System.getProperty("maven.repo.local") // runs the plugin description generator task generatePluginDescriptor(type: Exec, dependsOn: generatePomFile) { - def pluginDescriptorFile = new File(outputDir, 'META-INF/maven/plugin.xml') + def pluginDescriptorFile = outputDir.file('META-INF/maven/plugin.xml') workingDir projectDir boolean isWindows = System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0 @@ -100,8 +85,9 @@ task generatePluginDescriptor(type: Exec, dependsOn: generatePomFile) { ]) commandLine args doLast { - assert pluginDescriptorFile.file, "$pluginDescriptorFile: was not generated" - logger.info("Plugin descriptor is generated in $pluginDescriptorFile") + def descriptorFile = pluginDescriptorFile.get().getAsFile() + assert descriptorFile, "$descriptorFile: was not generated" + logger.info("Plugin descriptor is generated in $descriptorFile") } } diff --git a/atomicfu-maven-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/TransformMojo.kt b/atomicfu-maven-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/TransformMojo.kt index 28580d8..5f45f3e 100644 --- a/atomicfu-maven-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/TransformMojo.kt +++ b/atomicfu-maven-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/TransformMojo.kt @@ -17,7 +17,7 @@ package kotlinx.atomicfu.plugin import kotlinx.atomicfu.transformer.AtomicFUTransformer -import kotlinx.atomicfu.transformer.Variant +import kotlinx.atomicfu.transformer.JvmVariant import org.apache.maven.plugin.AbstractMojo import org.apache.maven.plugins.annotations.LifecyclePhase import org.apache.maven.plugins.annotations.Mojo @@ -50,8 +50,8 @@ class TransformMojo : AbstractMojo() { /** * Transformation variant: "FU", "VH", or "BOTH". */ - @Parameter(defaultValue = "FU", property = "variant", required = true) - lateinit var variant: Variant + @Parameter(defaultValue = "FU", property = "jvmVariant", required = true) + lateinit var jvmVariant: JvmVariant /** * Verbose debug info. @@ -60,7 +60,7 @@ class TransformMojo : AbstractMojo() { var verbose: Boolean = false override fun execute() { - val t = AtomicFUTransformer(classpath, input, output, variant) + val t = AtomicFUTransformer(classpath, input, output, jvmVariant) t.verbose = verbose t.transform() } diff --git a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AsmUtil.kt b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AsmUtil.kt index ba08fbc..0687f38 100644 --- a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AsmUtil.kt +++ b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AsmUtil.kt @@ -4,7 +4,9 @@ package kotlinx.atomicfu.transformer +import org.objectweb.asm.* import org.objectweb.asm.Opcodes.* +import org.objectweb.asm.Type.* import org.objectweb.asm.tree.* import org.objectweb.asm.util.* @@ -69,12 +71,23 @@ fun AbstractInsnNode.isGetField(owner: String) = fun AbstractInsnNode.isGetStatic(owner: String) = this is FieldInsnNode && this.opcode == GETSTATIC && this.owner == owner +fun AbstractInsnNode.isGetFieldOrGetStatic() = + this is FieldInsnNode && (this.opcode == GETFIELD || this.opcode == GETSTATIC) + fun AbstractInsnNode.isAreturn() = this.opcode == ARETURN fun AbstractInsnNode.isReturn() = this.opcode == RETURN +fun AbstractInsnNode.isTypeReturn(type: Type) = + opcode == when (type) { + INT_TYPE -> IRETURN + LONG_TYPE -> LRETURN + BOOLEAN_TYPE -> IRETURN + else -> ARETURN + } + fun AbstractInsnNode.isInvokeVirtual() = this.opcode == INVOKEVIRTUAL diff --git a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformer.kt b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformer.kt index c7c262c..a138422 100644 --- a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformer.kt +++ b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformer.kt @@ -91,7 +91,6 @@ private val TRACE_APPEND_3 = MethodId(TRACE_BASE_CLS, "append", getMethodDescrip private val TRACE_APPEND_4 = MethodId(TRACE_BASE_CLS, "append", getMethodDescriptor(VOID_TYPE, OBJECT_TYPE, OBJECT_TYPE, OBJECT_TYPE, OBJECT_TYPE), INVOKEVIRTUAL) private val TRACE_DEFAULT_ARGS = "I${OBJECT_TYPE.descriptor}" private const val DEFAULT = "\$default" -private const val DELEGATE = "\$delegate" private val TRACE_FACTORY = MethodId(TRACE_KT, TRACE, "(IL$AFU_PKG/$TRACE_FORMAT;)L$AFU_PKG/$TRACE_BASE;", INVOKESTATIC) private val TRACE_PARTIAL_ARGS_FACTORY = MethodId(TRACE_KT, "$TRACE$DEFAULT", "(IL$AFU_PKG/$TRACE_FORMAT;$TRACE_DEFAULT_ARGS)L$AFU_PKG/$TRACE_BASE;", INVOKESTATIC) @@ -126,7 +125,7 @@ private inline fun code(mv: MethodVisitor, block: InstructionAdapter.() -> Unit) } private inline fun insns(block: InstructionAdapter.() -> Unit): InsnList { - val node = MethodNode(ASM5) + val node = MethodNode(ASM9) block(InstructionAdapter(node)) return node.instructions } @@ -169,13 +168,13 @@ class FieldInfo( override fun toString(): String = "${owner.prettyStr()}::$name" } -enum class Variant { FU, VH, BOTH } +enum class JvmVariant { FU, VH, BOTH } class AtomicFUTransformer( classpath: List<String>, inputDir: File, outputDir: File = inputDir, - var variant: Variant = Variant.FU + var jvmVariant: JvmVariant = JvmVariant.FU ) : AtomicFUTransformerBase(inputDir, outputDir) { private val classPathLoader = URLClassLoader( @@ -188,6 +187,7 @@ class AtomicFUTransformer( private val traceFields = mutableSetOf<FieldId>() private val traceAccessors = mutableSetOf<MethodId>() private val fieldDelegates = mutableMapOf<FieldId, FieldInfo>() + private val delegatedPropertiesAccessors = mutableMapOf<FieldId, MethodId>() private val removeMethods = mutableSetOf<MethodId>() override fun transform() { @@ -195,7 +195,7 @@ class AtomicFUTransformer( val files = inputDir.walk().filter { it.isFile }.toList() val needTransform = analyzeFilesForFields(files) if (needTransform || outputDir == inputDir) { - val vh = variant == Variant.VH + val vh = jvmVariant == JvmVariant.VH // visit method bodies for external references to fields, runs all logic, fails if anything is wrong val needsTransform = analyzeFilesForRefs(files, vh) // perform transformation @@ -205,7 +205,7 @@ class AtomicFUTransformer( val outBytes = if (file.isClassFile() && file in needsTransform) transformFile(file, bytes, vh) else bytes val outFile = file.toOutputFile() outFile.mkdirsAndWrite(outBytes) - if (variant == Variant.BOTH && outBytes !== bytes) { + if (jvmVariant == JvmVariant.BOTH && outBytes !== bytes) { val vhBytes = transformFile(file, bytes, true) val vhFile = outputDir / "META-INF" / "versions" / "9" / file.relativeTo(inputDir).toString() vhFile.mkdirsAndWrite(vhBytes) @@ -280,7 +280,7 @@ class AtomicFUTransformer( return cw.toByteArray() // write transformed bytes } - private abstract inner class CV(cv: ClassVisitor?) : ClassVisitor(ASM5, cv) { + private abstract inner class CV(cv: ClassVisitor?) : ClassVisitor(ASM9, cv) { lateinit var className: String override fun visit( @@ -341,6 +341,10 @@ class AtomicFUTransformer( // check for copying atomic values into delegate fields and register potential delegate fields return DelegateFieldsCollectorMV(access, name, desc, signature, exceptions) } + // collect accessors of potential delegated properties + if (methodType.argumentTypes.isEmpty()) { + return DelegatedFieldAccessorCollectorMV(className, methodType.returnType, access, name, desc, signature, exceptions) + } return null } } @@ -348,7 +352,7 @@ class AtomicFUTransformer( private inner class AccessorCollectorMV( private val className: String, access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>? - ) : MethodNode(ASM5, access, name, desc, signature, exceptions) { + ) : MethodNode(ASM9, access, name, desc, signature, exceptions) { override fun visitEnd() { val insns = instructions.listUseful(4) if (insns.size == 3 && @@ -396,10 +400,47 @@ class AtomicFUTransformer( } } - private inner class DelegateFieldsCollectorMV( + private inner class DelegatedFieldAccessorCollectorMV( + private val className: String, private val returnType: Type, access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>? ) : MethodNode(ASM5, access, name, desc, signature, exceptions) { override fun visitEnd() { + // check for pattern of a delegated property getter + // getfield/getstatic a$delegate: Atomic* + // astore_i ... + // aload_i + // invokevirtual Atomic*.getValue() + // ireturn + var cur = instructions.first + while (cur != null && !(cur.isGetFieldOrGetStatic() && getType((cur as FieldInsnNode).desc) in AFU_TYPES)) { + cur = cur.next + } + if (cur != null && cur.next.opcode == ASTORE) { + val fi = cur as FieldInsnNode + val fieldDelegate = FieldId(className, fi.name, fi.desc) + val atomicType = getType(fi.desc) + val v = (cur.next as VarInsnNode).`var` + while (!(cur is VarInsnNode && cur.opcode == ALOAD && cur.`var` == v)) { + cur = cur.next + } + val invokeVirtual = cur.next + if (invokeVirtual.opcode == INVOKEVIRTUAL && (invokeVirtual as MethodInsnNode).name == GET_VALUE && invokeVirtual.owner == atomicType.internalName) { + // followed by RETURN operation + val next = invokeVirtual.nextUseful + val ret = if (next?.opcode == CHECKCAST) next.nextUseful else next + if (ret != null && ret.isTypeReturn(returnType)) { + // register delegated property accessor + delegatedPropertiesAccessors[fieldDelegate] = MethodId(className, name, desc, accessToInvokeOpcode(access)) + } + } + } + } + } + + private inner class DelegateFieldsCollectorMV( + access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>? + ) : MethodNode(ASM9, access, name, desc, signature, exceptions) { + override fun visitEnd() { // register delegate field and the corresponding original atomic field // getfield a: *Atomic // putfield a$delegate: *Atomic @@ -408,7 +449,7 @@ class AtomicFUTransformer( insn.checkGetFieldOrGetStatic()?.let { getfieldId -> val next = insn.next (next as? FieldInsnNode)?.checkPutFieldOrPutStatic()?.let { delegateFieldId -> - if (delegateFieldId.name.endsWith(DELEGATE)) { + if (getfieldId in fields && delegateFieldId in fields) { // original atomic value is copied to the synthetic delegate atomic field <delegated field name>$delegate val originalField = fields[getfieldId]!! fieldDelegates[delegateFieldId] = originalField @@ -420,11 +461,12 @@ class AtomicFUTransformer( val methodId = MethodId(insn.owner, insn.name, insn.desc, insn.opcode) if (methodId in FACTORIES) { (insn.nextUseful as? FieldInsnNode)?.checkPutFieldOrPutStatic()?.let { delegateFieldId -> - if (delegateFieldId.name.endsWith(DELEGATE)) { + val fieldType = getType(insn.desc).returnType + if (fieldType in AFU_TYPES) { + val isStatic = insn.nextUseful!!.opcode == PUTSTATIC // delegate field is initialized by a factory invocation - val fieldType = getType(insn.desc).returnType // for volatile delegated properties store FieldInfo of the delegate field itself - fieldDelegates[delegateFieldId] = FieldInfo(delegateFieldId, fieldType) + fieldDelegates[delegateFieldId] = FieldInfo(delegateFieldId, fieldType, isStatic) } } } @@ -447,6 +489,8 @@ class AtomicFUTransformer( return if (fieldId in fields) fieldId else null } + private fun FieldId.isFieldDelegate() = this in fieldDelegates && delegatedPropertiesAccessors.contains(this) + private inner class TransformerCV( cv: ClassVisitor?, private val vh: Boolean, @@ -460,7 +504,7 @@ class AtomicFUTransformer( private var originalClinit: MethodNode? = null private var newClinit: MethodNode? = null - private fun newClinit() = MethodNode(ASM5, ACC_STATIC, "<clinit>", "()V", null, null) + private fun newClinit() = MethodNode(ASM9, ACC_STATIC, "<clinit>", "()V", null, null) fun getOrCreateNewClinit(): MethodNode = newClinit ?: newClinit().also { newClinit = it } override fun visitSource(source: String?, debug: String?) { @@ -478,8 +522,8 @@ class AtomicFUTransformer( val fieldType = getType(desc) if (fieldType.sort == OBJECT && fieldType.internalName in AFU_CLASSES) { val fieldId = FieldId(className, name, desc) - // skip delegate field - if (fieldId in fieldDelegates && (fieldId != fieldDelegates[fieldId]!!.fieldId)) { + // skip field delegates except volatile delegated properties (e.g. val a: Int by atomic(0)) + if (fieldId.isFieldDelegate() && (fieldId != fieldDelegates[fieldId]!!.fieldId)) { transformed = true return null } @@ -607,7 +651,7 @@ class AtomicFUTransformer( val superMV = if (name == "<clinit>" && desc == "()V") { if (access and ACC_STATIC == 0) abort("<clinit> method not marked as static") // defer writing class initialization method - val node = MethodNode(ASM5, access, name, desc, signature, exceptions) + val node = MethodNode(ASM9, access, name, desc, signature, exceptions) if (originalClinit != null) abort("Multiple <clinit> methods found") originalClinit = node node @@ -673,7 +717,7 @@ class AtomicFUTransformer( private val packageName: String, private val vh: Boolean, private val analyzePhase2: Boolean // true in Phase 2 when we are analyzing file for refs (not transforming yet) - ) : MethodNode(ASM5, access, name, desc, signature, exceptions) { + ) : MethodNode(ASM9, access, name, desc, signature, exceptions) { init { this.mv = mv } @@ -715,14 +759,19 @@ class AtomicFUTransformer( i = i.next hasErrors = true } + // make sure all kotlinx/atomicfu references removed + removeAtomicReferencesFromLVT() // save transformed method if not in analysis phase if (!hasErrors && !analyzePhase2) accept(mv) } + private fun removeAtomicReferencesFromLVT() = + localVariables?.removeIf { getType(it.desc) in AFU_TYPES } + private fun FieldInsnNode.checkCopyToDelegate(): AbstractInsnNode? { val fieldId = FieldId(owner, name, desc) - if (fieldId in fieldDelegates) { + if (fieldId.isFieldDelegate()) { // original atomic value is copied to the synthetic delegate atomic field <delegated field name>$delegate val originalField = fieldDelegates[fieldId]!! val getField = previous as FieldInsnNode @@ -748,51 +797,29 @@ class AtomicFUTransformer( if (iv.name == GET_VALUE || iv.name == SET_VALUE) { check(!f.isArray || onArrayElement) { "getValue/setValue can only be called on elements of arrays" } val setInsn = iv.name == SET_VALUE - if (!onArrayElement) { - val primitiveType = f.getPrimitiveType(vh) - val owner = if (!vh && f.isStatic) f.refVolatileClassName else f.owner - if (!vh && f.isStatic) { - val getOwnerClass = FieldInsnNode( - GETSTATIC, - f.owner, - f.staticRefVolatileField, - getObjectType(owner).descriptor - ) - instructions.insert(ld, getOwnerClass) - } - instructions.remove(ld) // drop getstatic (we don't need field updater) - val j = FieldInsnNode( - when { - iv.name == GET_VALUE -> if (f.isStatic && vh) GETSTATIC else GETFIELD - else -> if (f.isStatic && vh) PUTSTATIC else PUTFIELD - }, owner, f.name, primitiveType.descriptor - ) - instructions.set(iv, j) // replace invokevirtual with get/setfield - return j.next + if (!onArrayElement) return getPureTypeField(ld, f, iv) + var methodType = getMethodType(iv.desc) + if (f.typeInfo.originalType != f.typeInfo.transformedType && !vh) { + val ret = f.typeInfo.transformedType.elementType + iv.desc = if (setInsn) getMethodDescriptor(methodType.returnType, ret) else getMethodDescriptor(ret, *methodType.argumentTypes) + methodType = getMethodType(iv.desc) + } + iv.name = iv.name.substring(0, 3) + if (!vh) { + // map to j.u.c.a.Atomic*Array get or set + iv.owner = descToName(f.fuType.descriptor) + iv.desc = getMethodDescriptor(methodType.returnType, INT_TYPE, *methodType.argumentTypes) } else { - var methodType = getMethodType(iv.desc) - if (f.typeInfo.originalType != f.typeInfo.transformedType && !vh) { - val ret = f.typeInfo.transformedType.elementType - iv.desc = if (setInsn) getMethodDescriptor(methodType.returnType, ret) else getMethodDescriptor(ret, *methodType.argumentTypes) - methodType = getMethodType(iv.desc) - } - iv.name = iv.name.substring(0, 3) - if (!vh) { - // map to j.u.c.a.Atomic*Array get or set - iv.owner = descToName(f.fuType.descriptor) - iv.desc = getMethodDescriptor(methodType.returnType, INT_TYPE, *methodType.argumentTypes) - } else { - // map to VarHandle get or set - iv.owner = descToName(VH_TYPE.descriptor) - iv.desc = getMethodDescriptor( - methodType.returnType, - f.getPrimitiveType(vh), - INT_TYPE, - *methodType.argumentTypes - ) - } - return iv + // map to VarHandle get or set + iv.owner = descToName(VH_TYPE.descriptor) + iv.desc = getMethodDescriptor( + methodType.returnType, + f.getPrimitiveType(vh), + INT_TYPE, + *methodType.argumentTypes + ) } + return iv } if (f.isArray && iv.name == GET_SIZE) { if (!vh) { @@ -855,6 +882,29 @@ class AtomicFUTransformer( return iv.next } + private fun getPureTypeField(ld: FieldInsnNode, f: FieldInfo, iv: MethodInsnNode): AbstractInsnNode? { + val primitiveType = f.getPrimitiveType(vh) + val owner = if (!vh && f.isStatic) f.refVolatileClassName else f.owner + if (!vh && f.isStatic) { + val getOwnerClass = FieldInsnNode( + GETSTATIC, + f.owner, + f.staticRefVolatileField, + getObjectType(owner).descriptor + ) + instructions.insert(ld, getOwnerClass) + } + instructions.remove(ld) // drop getfield/getstatic of the atomic field + val j = FieldInsnNode( + when { + iv.name == GET_VALUE -> if (f.isStatic && vh) GETSTATIC else GETFIELD + else -> if (f.isStatic && vh) PUTSTATIC else PUTFIELD + }, owner, f.name, primitiveType.descriptor + ) + instructions.set(iv, j) // replace invokevirtual with get/setfield + return j.next + } + private fun vhOperation(iv: MethodInsnNode, typeInfo: TypeInfo, f: FieldInfo) { val methodType = getMethodType(iv.desc) val args = methodType.argumentTypes @@ -1305,7 +1355,7 @@ class AtomicFUTransformer( is FieldInsnNode -> { val fieldId = FieldId(i.owner, i.name, i.desc) if ((i.opcode == GETFIELD || i.opcode == GETSTATIC) && fieldId in fields) { - if (fieldId in fieldDelegates && i.next.opcode == ASTORE) { + if (fieldId.isFieldDelegate() && i.next.opcode == ASTORE) { return transformDelegatedFieldAccessor(i, fieldId) } (i.next as? FieldInsnNode)?.checkCopyToDelegate()?.let { return it } // atomic field is copied to delegate field @@ -1338,29 +1388,25 @@ class AtomicFUTransformer( return i.next } - private fun transformDelegatedFieldAccessor(i: FieldInsnNode, fieldId: FieldId): AbstractInsnNode { + private fun transformDelegatedFieldAccessor(i: FieldInsnNode, fieldId: FieldId): AbstractInsnNode? { val f = fieldDelegates[fieldId]!! - val astore = (i.next as? VarInsnNode) ?: abort("Method $name does not match the pattern of a delegated field accessor") - val v = astore.`var` - var cur: AbstractInsnNode = i + val v = (i.next as VarInsnNode).`var` + // remove instructions [astore_v .. aload_v] + var cur: AbstractInsnNode = i.next while (!(cur is VarInsnNode && cur.opcode == ALOAD && cur.`var` == v)) { val next = cur.next instructions.remove(cur) cur = next } - val invokeVirtual = FlowAnalyzer(cur.next).execute() - instructions.remove(cur) - check(invokeVirtual.isAtomicGetValueOrSetValue()) { "Aload of the field delegate $f should be followed with Atomic*.getValue()/setValue() invocation" } - val accessorName = (invokeVirtual as MethodInsnNode).name.substring(0, 3) - val isGetter = accessorName == "get" - val primitiveType = f.getPrimitiveType(vh) - val j = FieldInsnNode(if (isGetter) GETFIELD else PUTFIELD, f.owner, f.name, primitiveType.descriptor) - instructions.set(invokeVirtual, j) + val iv = FlowAnalyzer(cur.next).execute() + check(iv.isAtomicGetValueOrSetValue()) { "Aload of the field delegate $f should be followed with Atomic*.getValue()/setValue() invocation" } + val isGetter = (iv as MethodInsnNode).name == GET_VALUE + instructions.remove(cur) // remove aload_v localVariables.removeIf { !(getType(it.desc).internalName == f.owner || (!isGetter && getType(it.desc) == getType(desc).argumentTypes.first() && it.name == "<set-?>")) } - return j.next + return getPureTypeField(i, f, iv) } private fun AbstractInsnNode.isAtomicGetFieldOrGetStatic() = @@ -1466,7 +1512,7 @@ fun main(args: Array<String>) { } val t = AtomicFUTransformer(emptyList(), File(args[0])) if (args.size > 1) t.outputDir = File(args[1]) - if (args.size > 2) t.variant = enumValueOf(args[2].toUpperCase(Locale.US)) + if (args.size > 2) t.jvmVariant = enumValueOf(args[2].toUpperCase(Locale.US)) t.verbose = true t.transform() } diff --git a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformerJS.kt b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformerJS.kt index 91f3037..de8f94a 100644 --- a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformerJS.kt +++ b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/AtomicFUTransformerJS.kt @@ -52,8 +52,8 @@ class AtomicFUTransformerJS( outputDir: File ) : AtomicFUTransformerBase(inputDir, outputDir) { private val atomicConstructors = mutableSetOf<String>() - private val fieldDelegates = mutableMapOf<String, String>() - private val delegatedProperties = mutableMapOf<String, String>() + private val delegateToOriginalAtomicField = mutableMapOf<String, Name>() + private val topLevelDelegatedFieldAccessorToOriginalField = mutableMapOf<String, Name>() private val atomicArrayConstructors = mutableMapOf<String, String?>() private val traceConstructors = mutableSetOf<String>() private val traceFormatObjects = mutableSetOf<String>() @@ -81,6 +81,7 @@ class AtomicFUTransformerJS( root.visit(AtomicConstructorDetector()) root.visit(FieldDelegatesVisitor()) root.visit(DelegatedPropertyAccessorsVisitor()) + root.visit(TopLevelDelegatedFieldsAccessorVisitor()) root.visit(TransformVisitor()) root.visit(AtomicOperationsInliner()) return root.eraseGetValue().toByteArray() @@ -283,12 +284,17 @@ class AtomicFUTransformerJS( if (stmt is ExpressionStatement) { if (stmt.expression is Assignment) { val delegateAssignment = stmt.expression as Assignment - if (delegateAssignment.right is PropertyGet) { - val initializer = delegateAssignment.right as PropertyGet - if (initializer.toSource() == atomicField.toSource()) { - // register field delegate and the original atomic field - fieldDelegates[(delegateAssignment.left as PropertyGet).property.toSource()] = - (atomicField as PropertyGet).property.toSource() + val initializer = delegateAssignment.right + if (initializer.toSource() == atomicField.toSource()) { + if (delegateAssignment.right is PropertyGet) { // initialization of a class field + // delegate${owner_class} to original atomic field + val delegateFieldName = (delegateAssignment.left as PropertyGet).property.toSource() + val ownerClassName = constructorBlock.enclosingFunction.functionName.identifier + delegateToOriginalAtomicField["$delegateFieldName\$$ownerClassName"] = + (atomicField as PropertyGet).property + } else { // top-level delegated fields + val delegateFieldName = delegateAssignment.left.toSource() + delegateToOriginalAtomicField[delegateFieldName] = atomicField as Name } } } @@ -303,22 +309,74 @@ class AtomicFUTransformerJS( inner class DelegatedPropertyAccessorsVisitor : NodeVisitor { override fun visit(node: AstNode?): Boolean { - if (node is PropertyGet) { - if (node.target is PropertyGet) { - if ((node.target as PropertyGet).property.toSource() in fieldDelegates && node.property.toSource() == MANGLED_VALUE_PROP) { - if (node.parent is ReturnStatement) { - val getter = ((((node.parent.parent as? Block)?.parent as? FunctionNode)?.parent as? ObjectProperty)?.parent as? ObjectLiteral) - ?: abort("Incorrect tree structure of the accessor for the property delegated " + - "to the atomic field ${fieldDelegates[node.target.toSource()]}") - val definePropertyCall = getter.parent as FunctionCall - val stringLiteral = definePropertyCall.arguments[1] as? StringLiteral - ?: abort ("Object.defineProperty invocation should take a property name as the second argument") - val delegatedProperty = stringLiteral.value.toString() - delegatedProperties[delegatedProperty] = (node.target as PropertyGet).property.toSource() + // find ObjectLiteral with accessors of the delegated field (get: FunctionNode, set: FunctionNode) + // redirect getter/setter from generated delegate field to the original atomic field + if (node is ObjectLiteral && node.parent is FunctionCall && + ((node.elements.size == 2 && node.elements[1].left.toSource() == "get") || + (node.elements.size == 3 && node.elements[1].left.toSource() == "get" && node.elements[2].left.toSource() == "set"))) { + // check that these are accessors of the atomic delegate field (check only getter) + if (node.elements[1].right is FunctionNode) { + val getter = node.elements[1].right as FunctionNode + if (getter.body.hasChildren() && getter.body.firstChild is ReturnStatement) { + val returnStmt = getter.body.firstChild as ReturnStatement + if (returnStmt.returnValue is PropertyGet && (returnStmt.returnValue as PropertyGet).property.toSource() == MANGLED_VALUE_PROP) { + val delegateField = ((returnStmt.returnValue as PropertyGet).target as PropertyGet).property.toSource() + val ownerClassName = ((node.parent as FunctionCall).arguments[0] as PropertyGet).target.toSource() + val key = "$delegateField\$$ownerClassName" + delegateToOriginalAtomicField[key]?.let { atomicField -> + // get() = a$delegate.value -> _a.value + getter.replaceAccessedField(true, atomicField) + if (node.elements.size == 3) { + // set(v: T) { a$delegate.value = v } -> { _a.value = v } + val setter = node.elements[2].right as FunctionNode + setter.replaceAccessedField(false, atomicField) + } + } + } + } + } + } + if (node is ObjectLiteral && node.parent is FunctionCall && ((node.elements.size == 1 && node.elements[0].left.toSource() == "get") || + node.elements.size == 2 && node.elements[0].left.toSource() == "get" && node.elements[1].left.toSource() == "set")) { + val parent = node.parent as FunctionCall + if (parent.arguments.size == 3 && parent.arguments[1] is StringLiteral) { + val topLevelDelegatedFieldName = (parent.arguments[1] as StringLiteral).value + if (topLevelDelegatedFieldName in delegateToOriginalAtomicField) { + val originalAtomicFieldName = delegateToOriginalAtomicField[topLevelDelegatedFieldName]!! + val getterName = node.elements[0].right.toSource() + topLevelDelegatedFieldAccessorToOriginalField[getterName] = originalAtomicFieldName + if (node.elements.size == 2) { + val setterName = node.elements[1].right.toSource() + topLevelDelegatedFieldAccessorToOriginalField[setterName] = originalAtomicFieldName } } } + } + return true + } + } + private fun FunctionNode.replaceAccessedField(isGetter: Boolean, newField: Name) { + val propertyGet = if (isGetter) { + (body.firstChild as ReturnStatement).returnValue as PropertyGet + } else { + ((body.firstChild as ExpressionStatement).expression as Assignment).left as PropertyGet + } + if (propertyGet.target is PropertyGet) { // class member + (propertyGet.target as PropertyGet).property = newField + } else { // top-level field + propertyGet.target = newField + } + } + + inner class TopLevelDelegatedFieldsAccessorVisitor : NodeVisitor { + override fun visit(node: AstNode?): Boolean { + if (node is FunctionNode && node.name.toString() in topLevelDelegatedFieldAccessorToOriginalField) { + val accessorName = node.name.toString() + val atomicField = topLevelDelegatedFieldAccessorToOriginalField[accessorName]!! + // function get_topLevelDelegatedField() = a.value -> _a.value + // function set_topLevelDelegatedField(v: T) { a.value = v } -> { _a.value = v } + node.replaceAccessedField(accessorName.startsWith("get"), atomicField) } return true } @@ -388,12 +446,6 @@ class AtomicFUTransformerJS( rr.receiver?.let { node.target = it } } } - if (node.property.toSource() in delegatedProperties) { - // replace delegated property name with the name of the original atomic field - val fieldDelegate = delegatedProperties[node.property.toSource()] - val originalField = fieldDelegates[fieldDelegate]!! - node.property = Name().apply { identifier = originalField } - } // replace Atomic*Array.size call with `length` property on the pure type js array if (node.property.toSource() == ARRAY_SIZE) { node.property = Name().also { it.identifier = LENGTH } diff --git a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/MetadataTransformer.kt b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/MetadataTransformer.kt index f8bcaf1..13c5366 100644 --- a/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/MetadataTransformer.kt +++ b/atomicfu-transformer/src/main/kotlin/kotlinx/atomicfu/transformer/MetadataTransformer.kt @@ -26,7 +26,6 @@ class MetadataTransformer( val hdr = KotlinClassHeader( kind = map["k"] as Int?, metadataVersion = (map["mv"] as? List<Int>)?.toIntArray(), - bytecodeVersion = (map["bv"] as? List<Int>)?.toIntArray(), data1 = (map["d1"] as? List<String>)?.toTypedArray(), data2 = (map["d2"] as? List<String>)?.toTypedArray(), extraString = map["xs"] as String?, @@ -229,4 +228,4 @@ private fun AnnotationNode.setKey(key: String, value: Any?) { } } error("Annotation key '$key' is not found") -}
\ No newline at end of file +} diff --git a/atomicfu/build.gradle b/atomicfu/build.gradle index 66cfcb8..063c9a3 100644 --- a/atomicfu/build.gradle +++ b/atomicfu/build.gradle @@ -18,12 +18,15 @@ ext { } kotlin { - targets.metaClass.addTarget = { preset -> - addNative(delegate.fromPreset(preset, preset.name)) + targets { + delegate.metaClass.addTarget = { preset -> + addNative(delegate.fromPreset(preset, preset.name)) + } } // JS -- always js { + moduleName = "kotlinx-atomicfu" // TODO: Commented out because browser tests do not work on TeamCity // browser() nodejs() @@ -89,6 +92,11 @@ if (rootProject.ext.native_targets_enabled) { addTarget(presets.watchosArm64) addTarget(presets.watchosX86) addTarget(presets.watchosX64) + + addTarget(presets.iosSimulatorArm64) + addTarget(presets.watchosSimulatorArm64) + addTarget(presets.tvosSimulatorArm64) + addTarget(presets.macosArm64) } } @@ -159,14 +167,6 @@ tasks.withType(compileJsLegacy.getClass()) { } } -compileJsLegacy.configure { - kotlinOptions { - // NOTE: Module base-name must be equal to the package name declared in package.json - def baseName = "kotlinx-atomicfu" - outputFile = new File(outputFile.parent, baseName + ".js") - } -} - apply from: file("$rootProject.projectDir/gradle/node-js.gradle") apply from: file("$rootProject.projectDir/gradle/publish-npm-js.gradle") @@ -188,7 +188,7 @@ compileTestJsLegacy.configure { kotlinOptions { // NOTE: Module base-name must be equal to the package name declared in package.json def baseName = "kotlinx-atomicfu" - outputFile = new File(outputFile.parent, baseName + ".js") + outputFile = new File(new File(outputFile).parent, baseName + ".js") } } def originalJsFile = compileTestJsLegacy.kotlinOptions.outputFile @@ -203,8 +203,14 @@ task transformJS(type: JavaExec, dependsOn: [compileTestJsLegacy]) { if (project.tasks.findByName('jsLegacyNodeTest')) { jsLegacyNodeTest.dependsOn transformJS + jsLegacyNodeTest.configure { + inputFileProperty.set(new File(transformedJsFile)) + } } else { jsNodeTest.dependsOn transformJS + jsNodeTest.configure { + inputFileProperty.set(new File(transformedJsFile)) + } } // ==== CONFIGURE JVM ===== @@ -216,7 +222,7 @@ def classesPostTransformBOTH = file("$buildDir/classes/kotlin/jvm/postTransforme tasks.withType(compileTestKotlinJvm.getClass()) { kotlinOptions { - jvmTarget = "1.6" + jvmTarget = "1.8" } } @@ -244,24 +250,6 @@ task transformVH(type: JavaExec, dependsOn: compileTestKotlinJvm) { outputs.dir(classesPostTransformVH) } -task checkJdk16() { - doLast { - if (!System.env.JDK_16) { - throw new GradleException("JDK_16 environment variable is not defined. " + - "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + - "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") - } - } -} - -task transformedTestFU_6(type: Test, dependsOn: [checkJdk16, transformFU]) { - executable = "$System.env.JDK_16/bin/java" - classpath = configurations.jvmTestRuntimeClasspath + project.files(classesPostTransformFU) - testClassesDirs = project.files(classesPostTransformFU) - exclude '**/*LFTest.*', '**/TraceToStringTest.*', '**/AtomicfuReferenceJsTest.*' - filter { setFailOnNoMatchingTests(false) } // to run excluded tests in Idea -} - task transformedTestFU_current(type: Test, dependsOn: transformFU) { classpath = files(configurations.jvmTestRuntimeClasspath, classesPostTransformFU) testClassesDirs = project.files(classesPostTransformFU) @@ -269,14 +257,6 @@ task transformedTestFU_current(type: Test, dependsOn: transformFU) { filter { setFailOnNoMatchingTests(false) } } -task transformedTestBOTH_6(type: Test, dependsOn: [checkJdk16, transformBOTH]) { - executable = "$System.env.JDK_16/bin/java" - classpath = files(configurations.jvmTestRuntimeClasspath, classesPostTransformBOTH) - testClassesDirs = project.files(classesPostTransformBOTH) - exclude '**/*LFTest.*', '**/TraceToStringTest.*', '**/TopLevelGeneratedDeclarationsReflectionTest.*', '**/SyntheticFUFieldsTest.*', '**/AtomicfuReferenceJsTest.*' - filter { setFailOnNoMatchingTests(false) } -} - task transformedTestBOTH_current(type: Test, dependsOn: transformBOTH) { classpath = files(configurations.jvmTestRuntimeClasspath, classesPostTransformBOTH) testClassesDirs = project.files(classesPostTransformBOTH) @@ -305,9 +285,7 @@ task testAtomicfuReferenceJs(type: Test, dependsOn: [compileTestKotlinJvm, trans } task jvmTestAll(dependsOn: [ - transformedTestFU_6, transformedTestFU_current, - transformedTestBOTH_6, transformedTestBOTH_current, transformedTestVH, testAtomicfuReferenceJs]) @@ -319,6 +297,72 @@ tasks.withType(Test) { } } +task compileJavaModuleInfo(type: JavaCompile) { + def moduleName = "kotlinx.atomicfu" // this module's name + def compileKotlinJvm = kotlin.targets["jvm"].compilations["main"].compileKotlinTask + def sourceDir = file("src/jvmMain/java9/") + def targetDir = compileKotlinJvm.destinationDirectory.map { it.dir("../java9/") } + + // Use a Java 11 compiler for the module info. + javaCompiler.set(project.javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(11)) }) + + // Always compile kotlin classes before the module descriptor. + dependsOn(compileKotlinJvm) + + // Add the module-info source file. + source(sourceDir) + + // Also add the module-info.java source file to the Kotlin compile task. + // The Kotlin compiler will parse and check module dependencies, + // but it currently won't compile to a module-info.class file. + // Note that module checking only works on JDK 9+, + // because the JDK built-in base modules are not available in earlier versions. + def javaVersion = compileKotlinJvm.kotlinJavaToolchain.javaVersion.getOrNull() + if (javaVersion?.isJava9Compatible() == true) { + logger.info("Module-info checking is enabled; $compileKotlinJvm is compiled using Java $javaVersion") + compileKotlinJvm.source(sourceDir) + } else { + logger.info("Module-info checking is disabled") + } + + // Set the task outputs and destination dir + outputs.dir(targetDir) + destinationDirectory.set(targetDir) + + // Configure JVM compatibility + sourceCompatibility = JavaVersion.VERSION_1_9.toString() + targetCompatibility = JavaVersion.VERSION_1_9.toString() + + // Set the Java release version. + options.release.set(9) + + // Ignore warnings about using 'requires transitive' on automatic modules. + // not needed when compiling with recent JDKs, e.g. 17 + options.compilerArgs.add("-Xlint:-requires-transitive-automatic") + + // Patch the compileKotlinJvm output classes into the compilation so exporting packages works correctly. + options.compilerArgs.addAll(["--patch-module", "$moduleName=${compileKotlinJvm.destinationDirectory.get().getAsFile()}"]) + + // Use the classpath of the compileKotlinJvm task. + // Also ensure that the module path is used instead of classpath. + classpath = compileKotlinJvm.classpath + modularity.inferModulePath.set(true) + + doFirst { + logger.warn("Task destination directory: ${destinationDirectory.get().getAsFile()}") + } +} + +// Configure the JAR task so that it will include the compiled module-info class file. +tasks.named("jvmJar") { + manifest { + attributes(["Multi-Release": true]) + } + from(compileJavaModuleInfo) { + into("META-INF/versions/9/") + } +} + jvmTest { exclude "**/AtomicfuBytecodeTest*", "**/AtomicfuReferenceJsTest*", '**/TopLevelGeneratedDeclarationsReflectionTest.*', '**/SyntheticFUFieldsTest.*' // run them only for transformed code } @@ -337,3 +381,4 @@ afterEvaluate { tasks.matching { it.name == "generatePomFileForKotlinMultiplatformPublication" }.configureEach { dependsOn(tasks["generatePomFileForJvmPublication"]) } + diff --git a/atomicfu/src/commonTest/kotlin/kotlinx/atomicfu/test/DelegatedPropertiesTest.kt b/atomicfu/src/commonTest/kotlin/kotlinx/atomicfu/test/DelegatedPropertiesTest.kt index 1b24e6d..4521c09 100644 --- a/atomicfu/src/commonTest/kotlin/kotlinx/atomicfu/test/DelegatedPropertiesTest.kt +++ b/atomicfu/src/commonTest/kotlin/kotlinx/atomicfu/test/DelegatedPropertiesTest.kt @@ -5,8 +5,27 @@ package kotlinx.atomicfu.test import kotlinx.atomicfu.atomic import kotlin.test.* -class DelegatedProperties { +private val topLevelIntOriginalAtomic = atomic(77) +var topLevelIntDelegatedProperty: Int by topLevelIntOriginalAtomic + +private val _topLevelLong = atomic(55555555555) +var topLevelDelegatedPropertyLong: Long by _topLevelLong + +private val _topLevelBoolean = atomic(false) +var topLevelDelegatedPropertyBoolean: Boolean by _topLevelBoolean + +private val _topLevelRef = atomic(listOf("a", "b")) +var topLevelDelegatedPropertyRef: List<String> by _topLevelRef + +var vTopLevelInt by atomic(77) + +var vTopLevelLong by atomic(777777777) +var vTopLevelBoolean by atomic(false) + +var vTopLevelRef by atomic(listOf("a", "b")) + +class DelegatedProperties { private val _a = atomic(42) var a: Int by _a @@ -99,6 +118,76 @@ class DelegatedProperties { assertEquals(99, vRef.b.n) } + @Test + fun testTopLevelDelegatedPropertiesInt() { + assertEquals(77, topLevelIntDelegatedProperty) + topLevelIntOriginalAtomic.compareAndSet(77, 56) + assertEquals(56, topLevelIntDelegatedProperty) + topLevelIntDelegatedProperty = 88 + topLevelIntOriginalAtomic.compareAndSet(88, 66) + assertEquals(66, topLevelIntOriginalAtomic.value) + assertEquals(66, topLevelIntDelegatedProperty) + } + + @Test + fun testTopLevelDelegatedPropertiesLong() { + assertEquals(55555555555, topLevelDelegatedPropertyLong) + _topLevelLong.getAndIncrement() + assertEquals(55555555556, topLevelDelegatedPropertyLong) + topLevelDelegatedPropertyLong = 7777777777777 + assertTrue(_topLevelLong.compareAndSet(7777777777777, 66666666666)) + assertEquals(66666666666, _topLevelLong.value) + assertEquals(66666666666, topLevelDelegatedPropertyLong) + } + + @Test + fun testTopLevelDelegatedPropertiesBoolean() { + assertEquals(false, topLevelDelegatedPropertyBoolean) + _topLevelBoolean.lazySet(true) + assertEquals(true, topLevelDelegatedPropertyBoolean) + topLevelDelegatedPropertyBoolean = false + assertTrue(_topLevelBoolean.compareAndSet(false, true)) + assertEquals(true, _topLevelBoolean.value) + assertEquals(true, topLevelDelegatedPropertyBoolean) + } + + @Test + fun testTopLevelDelegatedPropertiesRef() { + assertEquals("b", topLevelDelegatedPropertyRef[1]) + _topLevelRef.lazySet(listOf("c")) + assertEquals("c", topLevelDelegatedPropertyRef[0]) + topLevelDelegatedPropertyRef = listOf("d", "e") + assertEquals("e", _topLevelRef.value[1]) + } + + @Test + fun testVolatileTopLevelInt() { + assertEquals(77, vTopLevelInt) + vTopLevelInt = 55 + assertEquals(110, vTopLevelInt * 2) + } + + @Test + fun testVolatileTopLevelLong() { + assertEquals(777777777, vTopLevelLong) + vTopLevelLong = 55 + assertEquals(55, vTopLevelLong) + } + + @Test + fun testVolatileTopLevelBoolean() { + assertEquals(false, vTopLevelBoolean) + vTopLevelBoolean = true + assertEquals(true, vTopLevelBoolean) + } + + @Test + fun testVolatileTopLevelRef() { + assertEquals("a", vTopLevelRef[0]) + vTopLevelRef = listOf("c") + assertEquals("c", vTopLevelRef[0]) + } + class A (val b: B) class B (val n: Int) } @@ -144,4 +233,25 @@ class ExposedDelegatedPropertiesAccessorsTest { cl.vInt = 99 assertEquals(99, cl.vInt) } +} + +class ClashedNamesTest { + private class A1 { + val _a = atomic(0) + val a: Int by _a + } + + private class A2 { + val _a = atomic(0) + val a: Int by _a + } + + @Test + fun testClashedDelegatedPropertiesNames() { + val a1Class = A1() + val a2Class = A2() + a1Class._a.compareAndSet(0, 77) + assertEquals(77, a1Class.a) + assertEquals(0, a2Class.a) + } }
\ No newline at end of file diff --git a/atomicfu/src/jvmMain/java9/module-info.java b/atomicfu/src/jvmMain/java9/module-info.java new file mode 100644 index 0000000..e68d750 --- /dev/null +++ b/atomicfu/src/jvmMain/java9/module-info.java @@ -0,0 +1,6 @@ +module kotlinx.atomicfu { + requires transitive kotlin.stdlib; + + exports kotlinx.atomicfu; + exports kotlinx.atomicfu.locks; +} diff --git a/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/AtomicFU.kt b/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/AtomicFU.kt index 20dc5f2..ddaf1dc 100644 --- a/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/AtomicFU.kt +++ b/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/AtomicFU.kt @@ -80,10 +80,8 @@ public actual class AtomicRef<T> internal constructor(value: T, val trace: Trace @Volatile public actual var value: T = value set(value) { - interceptor.beforeUpdate(this) field = value - if (trace !== TraceBase.None) trace { "set($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "set($value)" } } public actual inline operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value @@ -94,22 +92,16 @@ public actual class AtomicRef<T> internal constructor(value: T, val trace: Trace * Maps to [AtomicReferenceFieldUpdater.lazySet]. */ public actual fun lazySet(value: T) { - interceptor.beforeUpdate(this) FU.lazySet(this, value) - if (trace !== TraceBase.None) trace { "lazySet($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "lazySet($value)" } } /** * Maps to [AtomicReferenceFieldUpdater.compareAndSet]. */ public actual fun compareAndSet(expect: T, update: T): Boolean { - interceptor.beforeUpdate(this) val result = FU.compareAndSet(this, expect, update) - if (result) { - if (trace !== TraceBase.None) trace { "CAS($expect, $update)" } - interceptor.afterRMW(this, expect, update) - } + if (result && trace !== None) trace { "CAS($expect, $update)" } return result } @@ -117,10 +109,8 @@ public actual class AtomicRef<T> internal constructor(value: T, val trace: Trace * Maps to [AtomicReferenceFieldUpdater.getAndSet]. */ public actual fun getAndSet(value: T): T { - interceptor.beforeUpdate(this) val oldValue = FU.getAndSet(this, value) as T - if (trace !== TraceBase.None) trace { "getAndSet($value):$oldValue" } - interceptor.afterRMW(this, oldValue, value) + if (trace !== None) trace { "getAndSet($value):$oldValue" } return oldValue } @@ -155,35 +145,27 @@ public actual class AtomicBoolean internal constructor(v: Boolean, val trace: Tr public actual var value: Boolean get() = _value != 0 set(value) { - interceptor.beforeUpdate(this) _value = if (value) 1 else 0 - if (trace !== TraceBase.None) trace { "set($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "set($value)" } } /** * Maps to [AtomicIntegerFieldUpdater.lazySet]. */ public actual fun lazySet(value: Boolean) { - interceptor.beforeUpdate(this) val v = if (value) 1 else 0 FU.lazySet(this, v) - if (trace !== TraceBase.None) trace { "lazySet($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "lazySet($value)" } } /** * Maps to [AtomicIntegerFieldUpdater.compareAndSet]. */ public actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean { - interceptor.beforeUpdate(this) val e = if (expect) 1 else 0 val u = if (update) 1 else 0 val result = FU.compareAndSet(this, e, u) - if (result) { - if (trace !== TraceBase.None) trace { "CAS($expect, $update)" } - interceptor.afterRMW(this, expect, update) - } + if (result && trace !== None) trace { "CAS($expect, $update)" } return result } @@ -191,11 +173,9 @@ public actual class AtomicBoolean internal constructor(v: Boolean, val trace: Tr * Maps to [AtomicIntegerFieldUpdater.getAndSet]. */ public actual fun getAndSet(value: Boolean): Boolean { - interceptor.beforeUpdate(this) val v = if (value) 1 else 0 val oldValue = FU.getAndSet(this, v) - if (trace !== TraceBase.None) trace { "getAndSet($value):$oldValue" } - interceptor.afterRMW(this, (oldValue == 1), value) + if (trace !== None) trace { "getAndSet($value):$oldValue" } return oldValue == 1 } @@ -220,10 +200,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB @Volatile public actual var value: Int = value set(value) { - interceptor.beforeUpdate(this) field = value - if (trace !== TraceBase.None) trace { "set($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "set($value)" } } public actual inline operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = value @@ -234,22 +212,16 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.lazySet]. */ public actual fun lazySet(value: Int) { - interceptor.beforeUpdate(this) FU.lazySet(this, value) - if (trace !== TraceBase.None) trace { "lazySet($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "lazySet($value)" } } /** * Maps to [AtomicIntegerFieldUpdater.compareAndSet]. */ public actual fun compareAndSet(expect: Int, update: Int): Boolean { - interceptor.beforeUpdate(this) val result = FU.compareAndSet(this, expect, update) - if (result) { - if (trace !== TraceBase.None) trace { "CAS($expect, $update)" } - interceptor.afterRMW(this, expect, update) - } + if (result && trace !== None) trace { "CAS($expect, $update)" } return result } @@ -257,10 +229,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.getAndSet]. */ public actual fun getAndSet(value: Int): Int { - interceptor.beforeUpdate(this) val oldValue = FU.getAndSet(this, value) - if (trace !== TraceBase.None) trace { "getAndSet($value):$oldValue" } - interceptor.afterRMW(this, oldValue, value) + if (trace !== None) trace { "getAndSet($value):$oldValue" } return oldValue } @@ -268,10 +238,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.getAndIncrement]. */ public actual fun getAndIncrement(): Int { - interceptor.beforeUpdate(this) val oldValue = FU.getAndIncrement(this) - if (trace !== TraceBase.None) trace { "getAndInc():$oldValue" } - interceptor.afterRMW(this, oldValue, oldValue + 1) + if (trace !== None) trace { "getAndInc():$oldValue" } return oldValue } @@ -279,10 +247,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.getAndDecrement]. */ public actual fun getAndDecrement(): Int { - interceptor.beforeUpdate(this) val oldValue = FU.getAndDecrement(this) - if (trace !== TraceBase.None) trace { "getAndDec():$oldValue" } - interceptor.afterRMW(this, oldValue, oldValue - 1) + if (trace !== None) trace { "getAndDec():$oldValue" } return oldValue } @@ -290,10 +256,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.getAndAdd]. */ public actual fun getAndAdd(delta: Int): Int { - interceptor.beforeUpdate(this) val oldValue = FU.getAndAdd(this, delta) - if (trace !== TraceBase.None) trace { "getAndAdd($delta):$oldValue" } - interceptor.afterRMW(this, oldValue, oldValue + delta) + if (trace !== None) trace { "getAndAdd($delta):$oldValue" } return oldValue } @@ -301,10 +265,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.addAndGet]. */ public actual fun addAndGet(delta: Int): Int { - interceptor.beforeUpdate(this) val newValue = FU.addAndGet(this, delta) - if (trace !== TraceBase.None) trace { "addAndGet($delta):$newValue" } - interceptor.afterRMW(this, newValue - delta, newValue) + if (trace !== None) trace { "addAndGet($delta):$newValue" } return newValue } @@ -312,10 +274,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.incrementAndGet]. */ public actual fun incrementAndGet(): Int { - interceptor.beforeUpdate(this) val newValue = FU.incrementAndGet(this) - if (trace !== TraceBase.None) trace { "incAndGet():$newValue" } - interceptor.afterRMW(this, newValue - 1, newValue) + if (trace !== None) trace { "incAndGet():$newValue" } return newValue } @@ -323,10 +283,8 @@ public actual class AtomicInt internal constructor(value: Int, val trace: TraceB * Maps to [AtomicIntegerFieldUpdater.decrementAndGet]. */ public actual fun decrementAndGet(): Int { - interceptor.beforeUpdate(this) val newValue = FU.decrementAndGet(this) - if (trace !== TraceBase.None) trace { "decAndGet():$newValue" } - interceptor.afterRMW(this, newValue + 1, newValue) + if (trace !== None) trace { "decAndGet():$newValue" } return newValue } @@ -365,10 +323,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac @Volatile public actual var value: Long = value set(value) { - interceptor.beforeUpdate(this) field = value - if (trace !== TraceBase.None) trace { "set($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "set($value)" } } public actual inline operator fun getValue(thisRef: Any?, property: KProperty<*>): Long = value @@ -379,22 +335,16 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.lazySet]. */ public actual fun lazySet(value: Long) { - interceptor.beforeUpdate(this) FU.lazySet(this, value) - if (trace !== TraceBase.None) trace { "lazySet($value)" } - interceptor.afterSet(this, value) + if (trace !== None) trace { "lazySet($value)" } } /** * Maps to [AtomicLongFieldUpdater.compareAndSet]. */ public actual fun compareAndSet(expect: Long, update: Long): Boolean { - interceptor.beforeUpdate(this) val result = FU.compareAndSet(this, expect, update) - if (result) { - if (trace !== TraceBase.None) trace { "CAS($expect, $update)" } - interceptor.afterRMW(this, expect, update) - } + if (result && trace !== None) trace { "CAS($expect, $update)" } return result } @@ -402,10 +352,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.getAndSet]. */ public actual fun getAndSet(value: Long): Long { - interceptor.beforeUpdate(this) val oldValue = FU.getAndSet(this, value) - if (trace !== TraceBase.None) trace { "getAndSet($value):$oldValue" } - interceptor.afterRMW(this, oldValue, value) + if (trace !== None) trace { "getAndSet($value):$oldValue" } return oldValue } @@ -413,10 +361,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.getAndIncrement]. */ public actual fun getAndIncrement(): Long { - interceptor.beforeUpdate(this) val oldValue = FU.getAndIncrement(this) - if (trace !== TraceBase.None) trace { "getAndInc():$oldValue" } - interceptor.afterRMW(this, oldValue, oldValue + 1) + if (trace !== None) trace { "getAndInc():$oldValue" } return oldValue } @@ -424,10 +370,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.getAndDecrement]. */ public actual fun getAndDecrement(): Long { - interceptor.beforeUpdate(this) val oldValue = FU.getAndDecrement(this) - if (trace !== TraceBase.None) trace { "getAndDec():$oldValue" } - interceptor.afterRMW(this, oldValue, oldValue - 1) + if (trace !== None) trace { "getAndDec():$oldValue" } return oldValue } @@ -435,10 +379,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.getAndAdd]. */ public actual fun getAndAdd(delta: Long): Long { - interceptor.beforeUpdate(this) val oldValue = FU.getAndAdd(this, delta) - if (trace !== TraceBase.None) trace { "getAndAdd($delta):$oldValue" } - interceptor.afterRMW(this, oldValue, oldValue + delta) + if (trace !== None) trace { "getAndAdd($delta):$oldValue" } return oldValue } @@ -446,10 +388,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.addAndGet]. */ public actual fun addAndGet(delta: Long): Long { - interceptor.beforeUpdate(this) val newValue = FU.addAndGet(this, delta) - if (trace !== TraceBase.None) trace { "addAndGet($delta):$newValue" } - interceptor.afterRMW(this, newValue - delta, newValue) + if (trace !== None) trace { "addAndGet($delta):$newValue" } return newValue } @@ -457,10 +397,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.incrementAndGet]. */ public actual fun incrementAndGet(): Long { - interceptor.beforeUpdate(this) val newValue = FU.incrementAndGet(this) - if (trace !== TraceBase.None) trace { "incAndGet():$newValue" } - interceptor.afterRMW(this, newValue - 1, newValue) + if (trace !== None) trace { "incAndGet():$newValue" } return newValue } @@ -468,10 +406,8 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac * Maps to [AtomicLongFieldUpdater.decrementAndGet]. */ public actual fun decrementAndGet(): Long { - interceptor.beforeUpdate(this) val newValue = FU.decrementAndGet(this) - if (trace !== TraceBase.None) trace { "decAndGet():$newValue" } - interceptor.afterRMW(this, newValue + 1, newValue) + if (trace !== None) trace { "decAndGet():$newValue" } return newValue } @@ -494,4 +430,4 @@ public actual class AtomicLong internal constructor(value: Long, val trace: Trac private companion object { private val FU = AtomicLongFieldUpdater.newUpdater(AtomicLong::class.java, "value") } -}
\ No newline at end of file +} diff --git a/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/Interceptor.kt b/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/Interceptor.kt deleted file mode 100644 index 28d989b..0000000 --- a/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/Interceptor.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu - -import java.util.concurrent.locks.ReentrantLock - -internal var interceptor: AtomicOperationInterceptor = DefaultInterceptor - private set -private val interceptorLock = ReentrantLock() - -internal fun lockAndSetInterceptor(impl: AtomicOperationInterceptor) { - if (!interceptorLock.tryLock() || interceptor !== DefaultInterceptor) { - error("Interceptor is locked by another test: $interceptor") - } - interceptor = impl -} - -internal fun unlockAndResetInterceptor(impl: AtomicOperationInterceptor) { - check(interceptor === impl) { "Unexpected interceptor found: $interceptor" } - interceptor = DefaultInterceptor - interceptorLock.unlock() -} - -/** - * Interceptor for modifications of atomic variables. - */ -internal open class AtomicOperationInterceptor { - open fun <T> beforeUpdate(ref: AtomicRef<T>) {} - open fun beforeUpdate(ref: AtomicInt) {} - open fun beforeUpdate(ref: AtomicLong) {} - open fun beforeUpdate(ref: AtomicBoolean){} - open fun <T> afterSet(ref: AtomicRef<T>, newValue: T) {} - open fun afterSet(ref: AtomicInt, newValue: Int) {} - open fun afterSet(ref: AtomicLong, newValue: Long) {} - open fun afterSet(ref: AtomicBoolean, newValue: Boolean) {} - open fun <T> afterRMW(ref: AtomicRef<T>, oldValue: T, newValue: T) {} - open fun afterRMW(ref: AtomicInt, oldValue: Int, newValue: Int) {} - open fun afterRMW(ref: AtomicLong, oldValue: Long, newValue: Long) {} - open fun afterRMW(ref: AtomicBoolean, oldValue: Boolean, newValue: Boolean) {} -} - -private object DefaultInterceptor : AtomicOperationInterceptor() { - override fun toString(): String = "DefaultInterceptor" -} diff --git a/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/LockFreedomTestEnvironment.kt b/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/LockFreedomTestEnvironment.kt deleted file mode 100644 index 0208feb..0000000 --- a/atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/LockFreedomTestEnvironment.kt +++ /dev/null @@ -1,482 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("RedundantVisibilityModifier") - -package kotlinx.atomicfu - -import java.util.* -import java.util.concurrent.atomic.* -import java.util.concurrent.locks.* -import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.* - -private const val PAUSE_EVERY_N_STEPS = 1000 -private const val STALL_LIMIT_MS = 15_000L // 15s -private const val SHUTDOWN_CHECK_MS = 10L // 10ms - -private const val STATUS_DONE = Int.MAX_VALUE - -private const val MAX_PARK_NANOS = 1_000_000L // part for at most 1ms just in case of loosing unpark signal - -/** - * Environment for performing lock-freedom tests for lock-free data structures - * that are written with [atomic] variables. - */ -public open class LockFreedomTestEnvironment( - private val name: String, - private val allowSuspendedThreads: Int = 0 -) { - private val interceptor = Interceptor() - private val threads = mutableListOf<TestThread>() - private val performedOps = LongAdder() - private val uncaughtException = AtomicReference<Throwable?>() - private var started = false - private var performedResumes = 0 - - @Volatile - private var completed = false - private val onCompletion = mutableListOf<() -> Unit>() - - private val ueh = Thread.UncaughtExceptionHandler { t, e -> - synchronized(System.out) { - println("Uncaught exception in thread $t") - e.printStackTrace(System.out) - uncaughtException.compareAndSet(null, e) - } - } - - // status < 0 - inv paused thread id - // status >= 0 - no. of performed resumes so far (==last epoch) - // status == STATUS_DONE - done working - private val status = AtomicInteger() - private val globalPauseProgress = AtomicInteger() - private val suspendedThreads = ArrayList<TestThread>() - - @Volatile - private var isActive = true - - // ---------- API ---------- - - /** - * Starts lock-freedom test for a given duration in seconds, - * invoking [progress] every second (it will be invoked `seconds + 1` times). - */ - public fun performTest(seconds: Int, progress: () -> Unit = {}) { - check(isActive) { "Can perform test at most once on this instance" } - println("=== $name") - val minThreads = 2 + allowSuspendedThreads - check(threads.size >= minThreads) { "Must define at least $minThreads test threads" } - lockAndSetInterceptor(interceptor) - started = true - var nextTime = System.currentTimeMillis() - threads.forEach { thread -> - thread.setUncaughtExceptionHandler(ueh) - thread.lastOpTime = nextTime - thread.start() - } - try { - var second = 0 - while (uncaughtException.get() == null) { - waitUntil(nextTime) - println("--- $second: Performed ${performedOps.sum()} operations${resumeStr()}") - progress() - checkStalled() - if (++second > seconds) break - nextTime += 1000L - } - } finally { - complete() - } - println("------ Done with ${performedOps.sum()} operations${resumeStr()}") - progress() - } - - private fun complete() { - val activeNonPausedThreads: MutableMap<TestThread, Array<StackTraceElement>> = mutableMapOf() - val shutdownDeadline = System.currentTimeMillis() + STALL_LIMIT_MS - try { - completed = true - // perform custom completion blocks. For testing of things like channels, these custom completion - // blocks close all the channels, so that all suspended coroutines shall get resumed. - onCompletion.forEach { it() } - // signal shutdown to all threads (non-paused threads will terminate) - isActive = false - // wait for threads to terminate - while (System.currentTimeMillis() < shutdownDeadline) { - // Check all threads while shutting down: - // All terminated threads are considered to make progress for the purpose of resuming stalled ones - activeNonPausedThreads.clear() - for (t in threads) { - when { - !t.isAlive -> t.makeProgress(getPausedEpoch()) // not alive - makes progress - t.index.inv() == status.get() -> {} // active, paused -- skip - else -> { - val stackTrace = t.stackTrace - if (t.isAlive) activeNonPausedThreads[t] = stackTrace - } - } - } - if (activeNonPausedThreads.isEmpty()) break - checkStalled() - Thread.sleep(SHUTDOWN_CHECK_MS) - } - activeNonPausedThreads.forEach { (t, stackTrack) -> - println("=== $t had failed to shutdown in time") - stackTrack.forEach { println("\tat $it") } - } - } finally { - shutdown(shutdownDeadline) - } - // if no other exception was throws & we had threads that did not shut down -- still fails - if (activeNonPausedThreads.isNotEmpty()) error("Some threads had failed to shutdown in time") - } - - private fun shutdown(shutdownDeadline: Long) { - // forcefully unpause paused threads to shut them down (if any left) - val curStatus = status.getAndSet(STATUS_DONE) - if (curStatus < 0) LockSupport.unpark(threads[curStatus.inv()]) - threads.forEach { - val remaining = shutdownDeadline - System.currentTimeMillis() - if (remaining > 0) it.join(remaining) - } - // abort waiting threads (if still any left) - threads.forEach { it.abortWait() } - // cleanup & be done - unlockAndResetInterceptor(interceptor) - uncaughtException.get()?.let { throw it } - threads.find { it.isAlive }?.let { dumpThreadsError("A thread is still alive: $it")} - } - - private fun checkStalled() { - val stallLimit = System.currentTimeMillis() - STALL_LIMIT_MS - val stalled = threads.filter { it.lastOpTime < stallLimit } - if (stalled.isNotEmpty()) dumpThreadsError("Progress stalled in threads ${stalled.map { it.name }}") - } - - private fun resumeStr(): String { - val resumes = performedResumes - return if (resumes == 0) "" else " (pause/resumes $resumes)" - } - - private fun waitUntil(nextTime: Long) { - while (true) { - val curTime = System.currentTimeMillis() - if (curTime >= nextTime) break - Thread.sleep(nextTime - curTime) - } - } - - private fun dumpThreadsError(message: String) : Nothing { - val traces = threads.associate { it to it.stackTrace } - println("!!! $message") - println("=== Dumping live thread stack traces") - for ((thread, trace) in traces) { - if (trace.isEmpty()) continue - println("Thread \"${thread.name}\" ${thread.state}") - for (t in trace) println("\tat ${t.className}.${t.methodName}(${t.fileName}:${t.lineNumber})") - println() - } - println("===") - error(message) - } - - /** - * Returns true when test was completed. - * Sets to true before calling [onCompletion] blocks. - */ - public val isCompleted: Boolean get() = completed - - /** - * Performs a given block of code on test's completion - */ - public fun onCompletion(block: () -> Unit) { - onCompletion += block - } - - /** - * Creates a new test thread in this environment that is executes a given lock-free [operation] - * in a loop while this environment [isActive]. - */ - public fun testThread(name: String? = null, operation: suspend TestThread.() -> Unit): TestThread = - TestThread(name, operation) - - /** - * Test thread. - */ - @Suppress("LeakingThis") - public inner class TestThread internal constructor( - name: String?, - private val operation: suspend TestThread.() -> Unit - ) : Thread(composeThreadName(name)) { - internal val index: Int - - internal @Volatile var lastOpTime = 0L - internal @Volatile var pausedEpoch = -1 - - private val random = Random() - - // thread-local stuff - private var operationEpoch = -1 - private var progressEpoch = -1 - private var sink = 0 - - init { - check(!started) - index = threads.size - threads += this - } - - public override fun run() { - while (isActive) { - callOperation() - } - } - - /** - * Use it to insert an arbitrary intermission between lock-free operations. - */ - public inline fun <T> intermission(block: () -> T): T { - afterLockFreeOperation() - return try { block() } - finally { beforeLockFreeOperation() } - } - - @PublishedApi - internal fun beforeLockFreeOperation() { - operationEpoch = getPausedEpoch() - } - - @PublishedApi - internal fun afterLockFreeOperation() { - makeProgress(operationEpoch) - lastOpTime = System.currentTimeMillis() - performedOps.add(1) - } - - internal fun makeProgress(epoch: Int) { - if (epoch <= progressEpoch) return - progressEpoch = epoch - val total = globalPauseProgress.incrementAndGet() - if (total >= threads.size - 1) { - check(total == threads.size - 1) - check(globalPauseProgress.compareAndSet(threads.size - 1, 0)) - resumeImpl() - } - } - - /** - * Inserts random spin wait between multiple lock-free operations in [operation]. - */ - public fun randomSpinWaitIntermission() { - intermission { - if (random.nextInt(100) < 95) return // be quick, no wait 95% of time - do { - val x = random.nextInt(100) - repeat(x) { sink += it } - } while (x >= 90) - } - } - - internal fun stepImpl() { - if (random.nextInt(PAUSE_EVERY_N_STEPS) == 0) pauseImpl() - } - - internal fun pauseImpl() { - while (true) { - val curStatus = status.get() - if (curStatus < 0 || curStatus == STATUS_DONE) return // some other thread paused or done - pausedEpoch = curStatus + 1 - val newStatus = index.inv() - if (status.compareAndSet(curStatus, newStatus)) { - while (status.get() == newStatus) LockSupport.parkNanos(MAX_PARK_NANOS) // wait - return - } - } - } - - // ----- Lightweight support for suspending operations ----- - - private fun callOperation() { - beforeLockFreeOperation() - beginRunningOperation() - val result = operation.startCoroutineUninterceptedOrReturn(this, completion) - when { - result === Unit -> afterLockFreeOperation() // operation completed w/o suspension -- done - result === COROUTINE_SUSPENDED -> waitUntilCompletion() // operation had suspended - else -> error("Unexpected result of operation: $result") - } - try { - doneRunningOperation() - } catch(e: IllegalStateException) { - throw IllegalStateException("${e.message}; original start result=$result", e) - } - } - - private var runningOperation = false - private var result: Result<Any?>? = null - private var continuation: Continuation<Any?>? = null - - private fun waitUntilCompletion() { - try { - while (true) { - afterLockFreeOperation() - val result: Result<Any?> = waitForResult() - val continuation = takeContinuation() - if (continuation == null) { // done - check(result.getOrThrow() === Unit) - return - } - removeSuspended(this) - beforeLockFreeOperation() - continuation.resumeWith(result) - } - } finally { - removeSuspended(this) - } - } - - private fun beginRunningOperation() { - runningOperation = true - result = null - continuation = null - } - - @Synchronized - private fun doneRunningOperation() { - check(runningOperation) { "Should be running operation" } - check(result == null && continuation == null) { - "Callback invoked with result=$result, continuation=$continuation" - } - runningOperation = false - } - - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - @Synchronized - private fun resumeWith(result: Result<Any?>, continuation: Continuation<Any?>?) { - check(runningOperation) { "Should be running operation" } - check(this.result == null && this.continuation == null) { - "Resumed again with result=$result, continuation=$continuation, when this: result=${this.result}, continuation=${this.continuation}" - } - this.result = result - this.continuation = continuation - (this as Object).notifyAll() - } - - @Suppress("RESULT_CLASS_IN_RETURN_TYPE", "PLATFORM_CLASS_MAPPED_TO_KOTLIN") - @Synchronized - private fun waitForResult(): Result<Any?> { - while (true) { - val result = this.result - if (result != null) return result - val index = addSuspended(this) - if (index < allowSuspendedThreads) { - // This suspension was permitted, so assume progress is happening while it is suspended - makeProgress(getPausedEpoch()) - } - (this as Object).wait(10) // at most 10 ms - } - } - - @Synchronized - private fun takeContinuation(): Continuation<Any?>? = - continuation.also { - this.result = null - this.continuation = null - } - - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - @Synchronized - fun abortWait() { - this.result = Result.failure(IllegalStateException("Aborted at the end of test")) - (this as Object).notifyAll() - } - - private val interceptor: CoroutineContext = object : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { - override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = - Continuation<T>(this) { - @Suppress("UNCHECKED_CAST") - resumeWith(it, continuation as Continuation<Any?>) - } - } - - private val completion = Continuation<Unit>(interceptor) { - resumeWith(it, null) - } - } - - // ---------- Implementation ---------- - - @Synchronized - private fun addSuspended(thread: TestThread): Int { - val index = suspendedThreads.indexOf(thread) - if (index >= 0) return index - suspendedThreads.add(thread) - return suspendedThreads.size - 1 - } - - @Synchronized - private fun removeSuspended(thread: TestThread) { - suspendedThreads.remove(thread) - } - - private fun getPausedEpoch(): Int { - while (true) { - val curStatus = status.get() - if (curStatus >= 0) return -1 // not paused - val thread = threads[curStatus.inv()] - val pausedEpoch = thread.pausedEpoch - if (curStatus == status.get()) return pausedEpoch - } - } - - internal fun step() { - val thread = Thread.currentThread() as? TestThread ?: return - thread.stepImpl() - } - - private fun resumeImpl() { - while (true) { - val curStatus = status.get() - if (curStatus == STATUS_DONE) return // done - check(curStatus < 0) - val thread = threads[curStatus.inv()] - performedResumes = thread.pausedEpoch - if (status.compareAndSet(curStatus, thread.pausedEpoch)) { - LockSupport.unpark(thread) - return - } - } - } - - private fun composeThreadName(threadName: String?): String { - if (threadName != null) return "$name-$threadName" - return name + "-${threads.size + 1}" - } - - private inner class Interceptor : AtomicOperationInterceptor() { - override fun <T> beforeUpdate(ref: AtomicRef<T>) = step() - override fun beforeUpdate(ref: AtomicInt) = step() - override fun beforeUpdate(ref: AtomicLong) = step() - override fun <T> afterSet(ref: AtomicRef<T>, newValue: T) = step() - override fun afterSet(ref: AtomicInt, newValue: Int) = step() - override fun afterSet(ref: AtomicLong, newValue: Long) = step() - override fun <T> afterRMW(ref: AtomicRef<T>, oldValue: T, newValue: T) = step() - override fun afterRMW(ref: AtomicInt, oldValue: Int, newValue: Int) = step() - override fun afterRMW(ref: AtomicLong, oldValue: Long, newValue: Long) = step() - override fun toString(): String = "LockFreedomTestEnvironment($name)" - } -} - -/** - * Manual pause for on-going lock-free operation in a specified piece of code. - * Use it for targeted debugging of specific places in code. It does nothing - * when invoked outside of test thread. - * - * **Don't use it in production code.** - */ -public fun pauseLockFreeOp() { - val thread = Thread.currentThread() as? LockFreedomTestEnvironment.TestThread ?: return - thread.pauseImpl() -}
\ No newline at end of file diff --git a/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/test/LockFreeQueueLFTest.kt b/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/test/LockFreeQueueLFTest.kt deleted file mode 100644 index 5179638..0000000 --- a/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/test/LockFreeQueueLFTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.test - -import kotlinx.atomicfu.LockFreedomTestEnvironment -import org.junit.Test -import java.util.* - -class LockFreeQueueLFTest : LockFreedomTestEnvironment("LockFreeQueueLFTest") { - val nEnqueuers = 2 - val nDequeuers = 2 - val nSeconds = 5 - - val queue = LockFreeQueue() - - @Test - fun testLockFreedom() { - repeat(nEnqueuers) { id -> - val rnd = Random() - testThread("Enqueue-$id") { - queue.enqueue(rnd.nextInt(1000)) - } - } - repeat(nDequeuers) { id -> - testThread("Dequeue-$id") { - queue.dequeue() - } - } - performTest(nSeconds) - } -}
\ No newline at end of file diff --git a/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/test/TraceLFTest.kt b/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/test/TraceLFTest.kt deleted file mode 100644 index 32ea8d8..0000000 --- a/atomicfu/src/jvmTest/kotlin/kotlinx/atomicfu/test/TraceLFTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.atomicfu.test - -import kotlinx.atomicfu.* -import kotlin.test.Test - -class Counter { - private val t = Trace(64, TraceFormat { index, text -> - "$index: [${Thread.currentThread().name}] $text" - }) - private val a = atomic(0, t) - - fun inc(): Int { - t { "inc() invoked" } - val x = a.incrementAndGet() - t { "inc() = $x" } - return x - } - - internal fun get() = a.value -} - -class CounterDefaultAtomic { - private val a = atomic(0) - private val trace = Trace(64) - - fun inc(): Int { - trace { "inc() invoked" } - val x = a.incrementAndGet() - trace { "inc() = $x" } - return x - } - - internal fun get() = a.value -} - -class CounterLFTest : LockFreedomTestEnvironment("CounterLFTest") { - private val c = Counter() - private val c1 = CounterDefaultAtomic() - - @Test - fun testCounterDefault() { - repeat(10) { id -> - testThread ("Inc-$id") { - c1.inc() - } - } - repeat(2) { id -> - testThread("Get-$id") { - c1.get() - } - } - performTest(10) - println(c1.get()) - } - - @Test - fun testLockFreedom() { - repeat(10) { id -> - testThread("Inc-$id") { - c.inc() - } - } - repeat(2) { id -> - testThread("Get-$id") { - c.get() - } - } - performTest(10) - println(c.get()) - } -} - diff --git a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/AtomicFU.kt b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/AtomicFU.kt index 55ed452..b369540 100644 --- a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/AtomicFU.kt +++ b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/AtomicFU.kt @@ -25,8 +25,8 @@ public actual fun atomic(initial: Boolean): AtomicBoolean = atomic(initial, None // ==================================== AtomicRef ==================================== -@Suppress("ACTUAL_WITHOUT_EXPECT", "EXPERIMENTAL_FEATURE_WARNING", "NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") -public actual inline class AtomicRef<T> internal constructor(@PublishedApi internal val a: KAtomicRef<T>) { +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual value class AtomicRef<T> internal constructor(@PublishedApi internal val a: KAtomicRef<T>) { public actual inline var value: T get() = a.value set(value) { @@ -62,8 +62,8 @@ public actual inline class AtomicRef<T> internal constructor(@PublishedApi inter // ==================================== AtomicBoolean ==================================== -@Suppress("ACTUAL_WITHOUT_EXPECT", "EXPERIMENTAL_FEATURE_WARNING", "NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") -public actual inline class AtomicBoolean internal constructor(@PublishedApi internal val a: KAtomicInt) { +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual value class AtomicBoolean internal constructor(@PublishedApi internal val a: KAtomicInt) { public actual inline var value: Boolean get() = a.value != 0 set(value) { a.value = if (value) 1 else 0 } @@ -94,8 +94,8 @@ public actual inline class AtomicBoolean internal constructor(@PublishedApi inte // ==================================== AtomicInt ==================================== -@Suppress("ACTUAL_WITHOUT_EXPECT", "EXPERIMENTAL_FEATURE_WARNING", "NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") -public actual inline class AtomicInt internal constructor(@PublishedApi internal val a: KAtomicInt) { +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual value class AtomicInt internal constructor(@PublishedApi internal val a: KAtomicInt) { public actual inline var value: Int get() = a.value set(value) { a.value = value } @@ -132,8 +132,8 @@ public actual inline class AtomicInt internal constructor(@PublishedApi internal // ==================================== AtomicLong ==================================== -@Suppress("ACTUAL_WITHOUT_EXPECT", "EXPERIMENTAL_FEATURE_WARNING", "NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") -public actual inline class AtomicLong internal constructor(@PublishedApi internal val a: KAtomicLong) { +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual value class AtomicLong internal constructor(@PublishedApi internal val a: KAtomicLong) { public actual inline var value: Long get() = a.value set(value) { a.value = value } diff --git a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt index ff81a5a..76e5d7a 100644 --- a/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt +++ b/atomicfu/src/nativeMain/kotlin/kotlinx/atomicfu/locks/Synchronized.kt @@ -190,7 +190,7 @@ class MutexPool(capacity: Int) { init { for (i in 0 until capacity) { - release(interpretCPointer<mutex_node_t>(mutexes.rawValue.plus(i * mutex_node_t.size))!!) + release(interpretCPointer<mutex_node_t>(mutexes.rawValue.plus(i * sizeOf<mutex_node_t>()))!!) } } diff --git a/build.gradle b/build.gradle index 12f531d..09a62fe 100644 --- a/build.gradle +++ b/build.gradle @@ -34,15 +34,6 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } // Future replacement for kotlin-dev, with cache redirector maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } - maven { - url "https://kotlin.bintray.com/kotlin-dev" - credentials { - username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" - password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" - } - } - maven { url "https://kotlin.bintray.com/kotlin-eap" } - maven { url "https://jetbrains.bintray.com/kotlin-native-dependencies" } maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } } @@ -66,17 +57,8 @@ allprojects { println "Using Kotlin $kotlin_version for project $it" repositories { jcenter() - maven { url "https://kotlin.bintray.com/kotlin-eap" } // Future replacement for kotlin-dev, with cache redirector maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } - maven { - url "https://kotlin.bintray.com/kotlin-dev" - credentials { - username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" - password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" - } - } - maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } } diff --git a/buildSrc/src/main/kotlin/Publishing.kt b/buildSrc/src/main/kotlin/Publishing.kt index 6844ad7..3db911f 100644 --- a/buildSrc/src/main/kotlin/Publishing.kt +++ b/buildSrc/src/main/kotlin/Publishing.kt @@ -44,20 +44,6 @@ fun MavenPom.configureMavenCentralMetadata(project: Project) { } } -fun configureBintrayPublication(rh: RepositoryHandler, project: Project) { - rh.maven { - val user = "kotlin" - val repo = "kotlinx" - val name = "kotlinx.atomicfu" - url = URI("https://api.bintray.com/maven/$user/$repo/$name/;publish=0;override=0") - - credentials { - username = project.findProperty("bintrayUser") as? String ?: System.getenv("BINTRAY_USER") - password = project.findProperty("bintrayApiKey") as? String ?: System.getenv("BINTRAY_API_KEY") - } - } -} - fun mavenRepositoryUri(): URI { // TODO -SNAPSHOT detection can be made here as well val repositoryId: String? = System.getenv("libs.repository.id") diff --git a/gradle.properties b/gradle.properties index ce4f84c..903d60d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,14 +2,14 @@ # Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. # -version=0.16.0-SNAPSHOT +version=0.18.5-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.5.0 -asm_version=7.2 +kotlin_version=1.7.20 +asm_version=9.3 slf4j_version=1.8.0-alpha2 junit_version=4.12 -kotlinx_metadata_version=0.2.0 +kotlinx_metadata_version=0.5.0 maven_version=3.5.3 diff --git a/gradle/compile-options.gradle b/gradle/compile-options.gradle index e6b4432..b6ce532 100644 --- a/gradle/compile-options.gradle +++ b/gradle/compile-options.gradle @@ -21,8 +21,6 @@ ext.configureKotlin = { isMultiplatform -> languageSettings { apiVersion = "1.4" languageVersion = "1.4" - useExperimentalAnnotation("kotlin.Experimental") - useExperimentalAnnotation("kotlin.ExperimentalStdlibApi") } } } diff --git a/gradle/interop-as-source-set-klib.gradle b/gradle/interop-as-source-set-klib.gradle index 62f2b77..25cb0c2 100644 --- a/gradle/interop-as-source-set-klib.gradle +++ b/gradle/interop-as-source-set-klib.gradle @@ -7,7 +7,7 @@ project.ext.registerInteropAsSourceSetOutput = { interop, sourceSet -> def cinteropTask = tasks.named(interop.interopProcessingTaskName) def cinteropKlib = cinteropTask.map { it.outputFile } def fakeCinteropCompilation = kotlin.targets["metadata"].compilations[sourceSet.name] - def destination = fakeCinteropCompilation.compileKotlinTask.destinationDir + def destination = fakeCinteropCompilation.compileKotlinTask.destinationDirectory def tempDir = "$buildDir/tmp/${sourceSet.name}UnpackedInteropKlib" diff --git a/gradle/publish-npm-js.gradle b/gradle/publish-npm-js.gradle index e6760a7..b7e7264 100644 --- a/gradle/publish-npm-js.gradle +++ b/gradle/publish-npm-js.gradle @@ -29,7 +29,7 @@ task preparePublishNpm(type: Copy, dependsOn: [compileJsLegacy]) { from(npmTemplateDir) { expand (project.properties + [kotlinDependency: "\"kotlin\": \"$kotlin_version\""]) } - from(compileJsLegacy.destinationDir) + from(compileJsLegacy.destinationDirectory) into npmDeployDir } diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle index 0ca4788..e9e4372 100644 --- a/gradle/publishing.gradle +++ b/gradle/publishing.gradle @@ -27,15 +27,9 @@ task javadocJar(type: Jar) { archiveClassifier = 'javadoc' } -def bintrayUpload = System.getenv("libs.bintray.upload") != null - publishing { repositories { // this: closure - if (bintrayUpload) { - PublishingKt.configureBintrayPublication(delegate, project) - } else { - PublishingKt.configureMavenPublication(delegate, project) - } + PublishingKt.configureMavenPublication(delegate, project) } if (!isMultiplatform) { @@ -61,9 +55,7 @@ publishing { publications.all { PublishingKt.configureMavenCentralMetadata(pom, project) - if (!bintrayUpload) { - PublishingKt.signPublicationIfKeyPresent(project, it) - } + PublishingKt.signPublicationIfKeyPresent(project, it) // add empty javadocs if (it.name != "kotlinMultiplatform") { // The root module gets the JVM's javadoc JAR diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4c83d2a..0904b9b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip |