From 647cee67135757c8252781d549bf0c3f2244225e Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Thu, 28 Mar 2024 18:58:31 +0100 Subject: [PATCH 01/22] Done --- README.md | 26 -- bootstrap/build.gradle.kts | 27 ++ .../src/sun/nio/ch}/lincheck/EventTracker.kt | 12 +- .../src/sun/nio/ch/lincheck/Injections.java | 369 +++++++++++++++++ .../src/sun/nio/ch}/lincheck/TestThread.kt | 10 +- build.gradle.kts | 37 +- docs/topics/testing-strategies.md | 24 -- gradle.properties | 3 +- settings.gradle.kts | 4 +- .../kotlinx/lincheck/CTestConfiguration.kt | 7 + .../kotlinx/lincheck/ExecutionClassLoader.kt | 32 ++ .../jetbrains/kotlinx/lincheck/IdeaPlugin.kt | 2 + .../jetbrains/kotlinx/lincheck/Injections.kt | 375 ----------------- .../jetbrains/kotlinx/lincheck/LinChecker.kt | 8 +- .../kotlinx/lincheck/LincheckClassLoader.java | 236 ----------- .../kotlinx/lincheck/ObjectTraverser.kt | 9 +- .../jetbrains/kotlinx/lincheck/Reporter.kt | 3 +- .../org/jetbrains/kotlinx/lincheck/Result.kt | 2 +- .../org/jetbrains/kotlinx/lincheck/Utils.kt | 145 +------ .../runner/FixedActiveThreadsExecutor.kt | 24 +- .../lincheck/runner/ParallelThreadsRunner.kt | 32 +- .../kotlinx/lincheck/runner/Runner.kt | 18 +- .../managed}/AtomicFieldUpdaterNames.kt | 22 +- .../strategy/managed/JavaUtilRemapper.kt | 117 ------ .../strategy/managed/ManagedStrategy.kt | 95 +++-- .../lincheck/strategy/managed/TracePoint.kt | 7 +- .../ModelCheckingCTestConfiguration.kt | 6 + .../modelchecking/ModelCheckingStrategy.kt | 1 + .../stress/StressCTestConfiguration.kt | 5 + .../{CodeInformation.kt => CodeLocations.kt} | 24 +- .../transformation/LincheckClassVisitor.kt | 114 ++--- .../transformation/LincheckJavaAgent.kt | 391 ++++++++++++++++++ .../transformation/SafeClassWriter.java | 183 ++++++++ .../transformation/TransformationUtils.kt | 44 +- .../kotlinx/lincheck/verifier/LTS.kt | 9 +- .../generator/CustomGuaranteesTests.kt | 91 ++++ .../generator/ExpandingRangeGeneratorTest.kt | 5 +- .../FixedActiveThreadsExecutorIsolatedTest.kt | 2 +- .../isolated/ThreadDumpIsolatedTest.kt | 4 +- .../IllegalModuleAccessOutputMessageTest.kt | 47 --- .../ParallelThreadsRunnerExceptionTest.kt | 12 +- .../transformation/SerializableValueTests.kt | 17 - .../StaticInitializationDuringAnalysisTest.kt | 38 ++ ...rmInterfaceFromJUCWithRemappedClassTest.kt | 34 -- .../RendezvousChannelCustomTest.kt | 12 +- .../expected_logs/coroutine_cancellation.txt | 18 +- .../expected_logs/illegal_module_access.txt | 48 --- .../expected_logs/suspend_trace_reporting.txt | 72 ++-- 48 files changed, 1489 insertions(+), 1334 deletions(-) create mode 100644 bootstrap/build.gradle.kts rename {src/jvm/main/org/jetbrains/kotlinx => bootstrap/src/sun/nio/ch}/lincheck/EventTracker.kt (85%) create mode 100644 bootstrap/src/sun/nio/ch/lincheck/Injections.java rename {src/jvm/main/org/jetbrains/kotlinx => bootstrap/src/sun/nio/ch}/lincheck/TestThread.kt (88%) create mode 100644 src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.kt delete mode 100644 src/jvm/main/org/jetbrains/kotlinx/lincheck/Injections.kt delete mode 100644 src/jvm/main/org/jetbrains/kotlinx/lincheck/LincheckClassLoader.java rename src/jvm/main/org/jetbrains/kotlinx/lincheck/{ => strategy/managed}/AtomicFieldUpdaterNames.kt (74%) delete mode 100644 src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/JavaUtilRemapper.kt rename src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/{CodeInformation.kt => CodeLocations.kt} (90%) create mode 100644 src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt create mode 100644 src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java create mode 100644 src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/CustomGuaranteesTests.kt delete mode 100644 src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/IllegalModuleAccessOutputMessageTest.kt create mode 100644 src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/StaticInitializationDuringAnalysisTest.kt delete mode 100644 src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/TransformInterfaceFromJUCWithRemappedClassTest.kt delete mode 100644 src/jvm/test/resources/expected_logs/illegal_module_access.txt diff --git a/README.md b/README.md index c0ddcdf68..838dec284 100755 --- a/README.md +++ b/README.md @@ -38,32 +38,6 @@ dependencies { } ``` -### Java 9+ -To use model checking strategy for Java 9 and later, add the following JVM properties: - -```text ---add-opens", "java.base/java.lang=ALL-UNNAMED ---add-opens java.base/jdk.internal.misc=ALL-UNNAMED ---add-exports java.base/jdk.internal.util=ALL-UNNAMED ---add-exports java.base/sun.security.action=ALL-UNNAMED -``` - -They are required if the testing code uses classes from the `java.util` package since -some of them use `jdk.internal.misc.Unsafe` or similar internal classes under the hood. - -If you use Gradle, add the following lines to `build.gradle.kts`: - -``` -tasks.withType { - jvmArgs( - "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", - "--add-exports", "java.base/jdk.internal.util=ALL-UNNAMED", - "--add-exports", "java.base/sun.security.action=ALL-UNNAMED" - ) -} -``` - - ## Example The following Lincheck test easily finds a bug in the standard Java's `ConcurrentLinkedDeque`: diff --git a/bootstrap/build.gradle.kts b/bootstrap/build.gradle.kts new file mode 100644 index 000000000..350c22d3f --- /dev/null +++ b/bootstrap/build.gradle.kts @@ -0,0 +1,27 @@ +import org.jetbrains.kotlin.gradle.tasks.* + +plugins { + java + kotlin("jvm") +} + +repositories { + mavenCentral() +} + +sourceSets.main { + java.srcDirs("src") +} + +tasks.withType { + kotlinOptions.jvmTarget = "11" +} + +tasks.jar { + archiveFileName.set("bootstrap.jar") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/EventTracker.kt b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt similarity index 85% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/EventTracker.kt rename to bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt index 89bbd7381..efe341d67 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/EventTracker.kt +++ b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt @@ -8,7 +8,7 @@ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.jetbrains.kotlinx.lincheck +package sun.nio.ch.lincheck import java.util.* @@ -16,7 +16,7 @@ import java.util.* * Methods of this interface are called from the instrumented tested code during model-checking. * See [Injections] for the documentation. */ -internal interface EventTracker { +interface EventTracker { fun beforeLock(codeLocation: Int) fun lock(monitor: Any) fun unlock(monitor: Any, codeLocation: Int) @@ -30,6 +30,7 @@ internal interface EventTracker { fun beforeReadField(obj: Any, className: String, fieldName: String, codeLocation: Int): Boolean fun beforeReadFieldStatic(className: String, fieldName: String, codeLocation: Int) + fun beforeReadFinalFieldStatic(className: String) fun beforeReadArrayElement(array: Any, index: Int, codeLocation: Int): Boolean fun afterRead(value: Any?) @@ -46,8 +47,11 @@ internal interface EventTracker { fun getThreadLocalRandom(): Random fun randomNextInt(): Int - fun onNewObjectCreation(obj: Any) - fun onWriteToObjectFieldOrArrayCell(obj: Any, fieldOrArrayCellValue: Any?) + fun beforeNewObjectCreation(className: String) + fun afterNewObjectCreation(obj: Any) + + fun onWriteToObjectFieldOrArrayCell(receiver: Any, fieldOrArrayCellValue: Any?) + fun onWriteObjectToStaticField(fieldValue: Any?) // Methods required for the plugin integration diff --git a/bootstrap/src/sun/nio/ch/lincheck/Injections.java b/bootstrap/src/sun/nio/ch/lincheck/Injections.java new file mode 100644 index 000000000..7dc903fcb --- /dev/null +++ b/bootstrap/src/sun/nio/ch/lincheck/Injections.java @@ -0,0 +1,369 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2024 JetBrains s.r.o. + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed + * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package sun.nio.ch.lincheck; + +import java.util.Random; + +/** + * Methods of this object are called from the instrumented code. + */ +public class Injections { + public static final Object VOID_RESULT = new Object(); + // Used in the verification phase to store a suspended continuation. + public static Object lastSuspendedCancellableContinuationDuringVerification = null; + + public static void storeCancellableContinuation(Object cont) { + Thread t = Thread.currentThread(); + if (t instanceof TestThread) { + ((TestThread) t).suspendedContinuation = cont; + } else { + // We are in the verification phase. + lastSuspendedCancellableContinuationDuringVerification = cont; + } + } + + public static boolean enterIgnoredSection() { + Thread t = Thread.currentThread(); + if (!(t instanceof TestThread)) return false; + TestThread testThread = (TestThread) t; + if (testThread.inIgnoredSection) return false; + testThread.inIgnoredSection = true; + return true; + } + + public static void leaveIgnoredSection() { + Thread t = Thread.currentThread(); + if (t instanceof TestThread) { + ((TestThread) t).inIgnoredSection = false; + } + } + + public static boolean inTestingCode() { + Thread t = Thread.currentThread(); + if (t instanceof TestThread) { + TestThread testThread = (TestThread) t; + return testThread.inTestingCode && !testThread.inIgnoredSection; + } + return false; + } + + /** + * See [org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy.lock] for the explanation + * why we have beforeLock method. + * + * Creates a trace point which is used in the subsequent [beforeEvent] method call. + */ + public static void beforeLock(int codeLocation) { + getEventTracker().beforeLock(codeLocation); + } + + /** + * Called from instrumented code instead of the MONITORENTER instruction, + * but after [beforeEvent] method call, if the plugin is enabled. + */ + public static void lock(Object monitor) { + getEventTracker().lock(monitor); + } + + /** + * Called from instrumented code instead of the MONITOREXIT instruction. + */ + public static void unlock(Object monitor, int codeLocation) { + getEventTracker().unlock(monitor, codeLocation); + } + + /** + * Called from the instrumented code instead of `Unsafe.park`. + */ + public static void park(int codeLocation) { + getEventTracker().park(codeLocation); + } + + /** + * Called from the instrumented code instead of `Unsafe.unpark`. + */ + public static void unpark(Thread thread, int codeLocation) { + getEventTracker().unpark(thread, codeLocation); + } + + /** + * See [org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy.wait] for the explanation + * why we have beforeWait method. + * + * Creates a trace point which is used in the subsequent [beforeEvent] method call. + */ + public static void beforeWait(int codeLocation) { + getEventTracker().beforeWait(codeLocation); + } + + /** + * Called from the instrumented code instead of [Object.wait], + * but after [beforeEvent] method call, if the plugin is enabled. + */ + public static void wait(Object monitor) { + getEventTracker().wait(monitor, false); + } + + + /** + * Called from the instrumented code instead of [Object.wait] with timeout, + * but after [beforeEvent] method call, if the plugin is enabled. + */ + public static void waitWithTimeout(Object monitor) { + getEventTracker().wait(monitor, true); + } + + + /** + * Called from the instrumented code instead of [Object.notify]. + */ + public static void notify(Object monitor, int codeLocation) { + getEventTracker().notify(monitor, codeLocation, false); + } + + /** + * Called from the instrumented code instead of [Object.notify]. + */ + public static void notifyAll(Object monitor, int codeLocation) { + getEventTracker().notify(monitor, codeLocation, true); + } + + /** + * Called from the instrumented code replacing random `int` generation with a deterministic random value. + */ + public static int nextInt() { + return getEventTracker().randomNextInt(); + } + + /** + * Called from the instrumented code to replace `ThreadLocalRandom.nextInt(origin, bound)` with a deterministic random value. + */ + public static int nextInt2(int origin, int bound) { + var enteredIgnoredSection = enterIgnoredSection(); + try { + return deterministicRandom().nextInt(bound); + } finally { + if (enteredIgnoredSection) { + leaveIgnoredSection(); + } + } + } + + /** + * Called from the instrumented code to get a random instance that is deterministic and controlled by Lincheck. + */ + public static Random deterministicRandom() { + return getEventTracker().getThreadLocalRandom(); + } + + /** + * Called from the instrumented code to check whether the object is a [Random] instance. + */ + public static boolean isRandom(Object any) { + return any instanceof Random; + } + + /** + * Called from the instrumented code before each field read. + * + * @return whether the trace point was created + */ + public static boolean beforeReadField(Object obj, String className, String fieldName, int codeLocation) { + if (obj == null) return false; // Ignore, NullPointerException will be thrown + return getEventTracker().beforeReadField(obj, className, fieldName, codeLocation); + } + + /** + * Called from the instrumented code before any public static field read. + */ + public static void beforeReadFieldStatic(String className, String fieldName, int codeLocation) { + getEventTracker().beforeReadFieldStatic(className, fieldName, codeLocation); + } + + /** + * Called from the instrumented code before any public static field read. + * We need to track such reads to ensure that the corresponding objects + * are instrumented. + */ + public static void beforeReadFinalFieldStatic(String className) { + getEventTracker().beforeReadFinalFieldStatic(className); + } + + /** + * Called from the instrumented code before any array cell read. + * + * @return whether the trace point was created + */ + public static boolean beforeReadArray(Object array, int index, int codeLocation) { + if (array == null) return false; // Ignore, NullPointerException will be thrown + return getEventTracker().beforeReadArrayElement(array, index, codeLocation); + } + + /** + * Called from the instrumented code after each field read (final field reads can be ignored here). + */ + public static void afterRead(Object value) { + getEventTracker().afterRead(value); + } + + /** + * Called from the instrumented code before each field write. + * + * @return whether the trace point was created + */ + public static boolean beforeWriteField(Object obj, String className, String fieldName, Object value, int codeLocation) { + if (obj == null) return false; // Ignore, NullPointerException will be thrown + return getEventTracker().beforeWriteField(obj, className, fieldName, value, codeLocation); + } + + /** + * Called from the instrumented code before any public static field write. + */ + public static void beforeWriteFieldStatic(String className, String fieldName, Object value, int codeLocation) { + getEventTracker().beforeWriteFieldStatic(className, fieldName, value, codeLocation); + } + + /** + * Called from the instrumented code before any array cell write. + * + * @return whether the trace point was created + */ + public static boolean beforeWriteArray(Object array, int index, Object value, int codeLocation) { + if (array == null) return false; // Ignore, NullPointerException will be thrown + return getEventTracker().beforeWriteArrayElement(array, index, value, codeLocation); + } + + /** + * Called from the instrumented code before any write operation. + */ + public static void afterWrite() { + getEventTracker().afterWrite(); + } + + /** + * Called from the instrumented code before any method call. + * + * @param owner is `null` for public static methods. + */ + public static void beforeMethodCall(Object owner, String className, String methodName, int codeLocation, Object[] params) { + getEventTracker().beforeMethodCall(owner, className, methodName, codeLocation, params); + } + + /** + * Called from the instrumented code before any atomic method call. + * This is just an optimization of [beforeMethodCall] for trusted + * atomic constructs to avoid wrapping the invocations into try-finally blocks. + */ + public static void beforeAtomicMethodCall(Object owner, String methodName, int codeLocation, Object[] params) { + getEventTracker().beforeAtomicMethodCall(owner, methodName, codeLocation, params); + } + + /** + * Called from the instrumented code after any method successful call, i.e., without any exception. + */ + public static void onMethodCallFinishedSuccessfully(Object result) { + getEventTracker().onMethodCallFinishedSuccessfully(result); + } + + /** + * Called from the instrumented code after any method that returns void successful call, i.e., without any exception. + */ + public static void onMethodCallVoidFinishedSuccessfully() { + getEventTracker().onMethodCallFinishedSuccessfully(VOID_RESULT); + } + + /** + * Called from the instrumented code after any method call threw an exception + */ + public static void onMethodCallThrewException(Throwable t) { + getEventTracker().onMethodCallThrewException(t); + } + + /** + * Called from the instrumented code before NEW instruction + */ + public static void beforeNewObjectCreation(String className) { + getEventTracker().beforeNewObjectCreation(className); + } + + /** + * Called from the instrumented code after any object is created + */ + public static void afterNewObjectCreation(Object obj) { + getEventTracker().afterNewObjectCreation(obj); + } + + /** + * Called from the instrumented code after value assigned to any receiver field. + * Required to track local objects. + * + * @param receiver the object in whose field the entry is made + * @param fieldOrArrayCellValue the value written into [receiver] field + * @see [LocalObjectManager] + */ + public static void onWriteToObjectFieldOrArrayCell(Object receiver, Object fieldOrArrayCellValue) { + getEventTracker().onWriteToObjectFieldOrArrayCell(receiver, fieldOrArrayCellValue); + } + + /** + * Called from the instrumented code to replace [java.lang.Object.hashCode] method call with some + * deterministic value. + */ + public static int hashCodeDeterministic(Object obj) { + var hashCode = obj.hashCode(); + // This is a dirty hack to determine whether there is a + // custom hashCode() implementation or it is always delegated + // to System.identityHashCode(..). + // While this code is not robust in theory, it works + // fine in practice. + if (hashCode == System.identityHashCode(obj)) { + return identityHashCodeDeterministic(obj); + } else { + return hashCode; + } + } + + /** + * Called from the instrumented code to replace [java.lang.System.identityHashCode] method call with some + * deterministic value. + */ + public static int identityHashCodeDeterministic(Object obj) { + if (obj == null) return 0; + // TODO: easier to support when `javaagent` is merged + return 0; + } + + private static EventTracker getEventTracker() { + Thread currentThread = Thread.currentThread(); + if (currentThread instanceof TestThread) { + return ((TestThread) currentThread).eventTracker; + } + throw new RuntimeException("Current thread is not an instance of TestThread"); + } + + + // == Methods required for the IDEA Plugin integration == + + public static boolean shouldInvokeBeforeEvent() { + return getEventTracker().shouldInvokeBeforeEvent(); + } + + /** + * @param type type of the next event. Used only for debug purposes. + */ + public static int getNextEventId(String type) { + return getEventTracker().getEventId(); + } + + public static void setLastMethodCallEventId() { + getEventTracker().setLastMethodCallEventId(); + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/TestThread.kt b/bootstrap/src/sun/nio/ch/lincheck/TestThread.kt similarity index 88% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/TestThread.kt rename to bootstrap/src/sun/nio/ch/lincheck/TestThread.kt index 200c94d03..c8e5ed034 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/TestThread.kt +++ b/bootstrap/src/sun/nio/ch/lincheck/TestThread.kt @@ -8,9 +8,7 @@ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.jetbrains.kotlinx.lincheck - -import kotlinx.coroutines.CancellableContinuation +package sun.nio.ch.lincheck /** * When Lincheck runs a test, all threads should be instances of this [TestThread] class. @@ -21,10 +19,10 @@ import kotlinx.coroutines.CancellableContinuation * @property threadId The index of the thread. * @param block The Runnable object associated with the thread. */ -internal class TestThread( - testName: String, +class TestThread( + testName: String?, // nullable only to avoid using `kotlin.jvm.internal.Intrinsics` for the non-nullability check. val threadId: Int, - block: Runnable + block: Runnable? // nullable only to avoid using `kotlin.jvm.internal.Intrinsics` for the non-nullability check. ) : Thread(block, "Lincheck-${testName}-$threadId") { /** diff --git a/build.gradle.kts b/build.gradle.kts index 8eb31ae18..f42dd6fce 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,15 +42,17 @@ kotlin { val kotlinVersion: String by project val kotlinxCoroutinesVersion: String by project val asmVersion: String by project - val reflectionsVersion: String by project + val byteBuddyVersion: String by project dependencies { + compileOnly(project(":bootstrap")) api("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") api("org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion") api("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") api("org.ow2.asm:asm-commons:$asmVersion") api("org.ow2.asm:asm-util:$asmVersion") - api("org.reflections:reflections:$reflectionsVersion") + api("net.bytebuddy:byte-buddy:$byteBuddyVersion") + api("net.bytebuddy:byte-buddy-agent:$byteBuddyVersion") } } @@ -61,6 +63,7 @@ kotlin { val jctoolsVersion: String by project val mockkVersion: String by project dependencies { + implementation(project(":bootstrap")) implementation("junit:junit:$junitVersion") implementation("org.jctools:jctools-core:$jctoolsVersion") implementation("io.mockk:mockk:${mockkVersion}") @@ -89,8 +92,17 @@ sourceSets.test { } } +val bootstrapJar = tasks.register("bootstrapJar") { + dependsOn(":bootstrap:jar") + from(file("${project(":bootstrap").buildDir}/libs/bootstrap.jar")) + into(file("$buildDir/processedResources/jvm/main")) +} tasks { + named("processResources").configure { + dependsOn(bootstrapJar) + } + replace("jvmSourcesJar", Jar::class).run { from(sourceSets["main"].allSource) } @@ -98,12 +110,7 @@ tasks { fun Test.configureJvmTestCommon() { maxParallelForks = 1 maxHeapSize = "6g" - val extraArgs = mutableListOf( - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", - "--add-exports", "java.base/jdk.internal.util=ALL-UNNAMED", - "--add-exports", "java.base/sun.security.action=ALL-UNNAMED", - ) + val extraArgs = mutableListOf() val withEventIdSequentialCheck: String by project if (withEventIdSequentialCheck.toBoolean()) { extraArgs.add("-Dlincheck.debug.withEventIdSequentialCheck=true") @@ -159,6 +166,7 @@ tasks { } withType { + dependsOn(bootstrapJar) manifest { val inceptionYear: String by project val lastCopyrightYear: String by project @@ -227,3 +235,16 @@ fun XmlProvider.removeAllLicencesExceptOne(licenceName: String) { } } } + +// We need the Lincheck version in the runtime to check compatibility with the Plugin, +// so we save it into the file in the resources and retrieve in later. +val versionTxt by tasks.registering { + doLast { + val version: String by project + file("src/commonMain/resources/version.txt").writeText(version) + } +} + +tasks.named("processResources") { + dependsOn(versionTxt) +} diff --git a/docs/topics/testing-strategies.md b/docs/topics/testing-strategies.md index 5a674b53e..5e2f121aa 100644 --- a/docs/topics/testing-strategies.md +++ b/docs/topics/testing-strategies.md @@ -93,30 +93,6 @@ class CounterTest { } ``` -> To use model checking strategy for Java 9 and later, add the following JVM properties: -> -> ```text -> --add-opens java.base/jdk.internal.misc=ALL-UNNAMED -> --add-exports java.base/jdk.internal.util=ALL-UNNAMED -> ``` -> -> They are required if the testing code uses classes from the `java.util` package since -> some of them use `jdk.internal.misc.Unsafe` or similar internal classes under the hood. -> If you use Gradle, add the following lines to `build.gradle.kts`: -> -> ``` -> tasks.withType { -> jvmArgs( -> "--add-opens", "java.base/java.lang=ALL-UNNAMED", -> "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", -> "--add-exports", "java.base/jdk.internal.util=ALL-UNNAMED", -> "--add-exports", "java.base/sun.security.action=ALL-UNNAMED" -> ) -> } -> ``` -> -{type="tip"} - ### How model checking works {initial-collapse-state="collapsed"} Most bugs in complicated concurrent algorithms can be reproduced with classic interleavings, switching the execution from diff --git a/gradle.properties b/gradle.properties index 35a2bd6ca..d1064b786 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,8 +23,7 @@ kotlinVersion=1.9.21 kotlinxCoroutinesVersion=1.7.3 asmVersion=9.6 atomicfuVersion=0.20.2 -# For java.util.* remapping -reflectionsVersion=0.10.2 +byteBuddyVersion=1.14.12 junitVersion=4.13.1 jctoolsVersion=3.3.0 diff --git a/settings.gradle.kts b/settings.gradle.kts index 997f623db..fcb7a04f6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,4 +17,6 @@ plugins { } val name: String by settings -rootProject.name = name \ No newline at end of file +rootProject.name = name + +include(":bootstrap") \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt index 6807a1d2f..e3548d8ec 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* import org.jetbrains.kotlinx.lincheck.strategy.stress.* +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode import org.jetbrains.kotlinx.lincheck.verifier.* import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* import java.lang.reflect.* @@ -36,6 +37,12 @@ abstract class CTestConfiguration( val timeoutMs: Long, val customScenarios: List ) { + + /** + * Specifies the transformation required for this strategy. + */ + internal abstract val instrumentationMode: InstrumentationMode + abstract fun createStrategy( testClass: Class<*>, scenario: ExecutionScenario, validationFunction: Actor?, stateRepresentationMethod: Method?, verifier: Verifier diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.kt new file mode 100644 index 000000000..b7496adbc --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.kt @@ -0,0 +1,32 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2024 JetBrains s.r.o. + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed + * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.runner.TestThreadExecution + +/** + * This classloader is mostly used by runner to separate parallel iterations, + * and define generated [test executions][TestThreadExecution]. + */ +class ExecutionClassLoader : ClassLoader() { + fun defineClass(className: String?, bytecode: ByteArray): Class { + @Suppress("UNCHECKED_CAST") + return super.defineClass(className, bytecode, 0, bytecode.size) as Class + } + + override fun loadClass(name: String?): Class<*> = runInIgnoredSection { + return super.loadClass(name) + } + + override fun loadClass(name: String?, resolve: Boolean): Class<*> = runInIgnoredSection { + return super.loadClass(name, resolve) + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/IdeaPlugin.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/IdeaPlugin.kt index 272722bbb..2f5fb2d46 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/IdeaPlugin.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/IdeaPlugin.kt @@ -10,6 +10,8 @@ */ package org.jetbrains.kotlinx.lincheck + +import sun.nio.ch.lincheck.* import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Injections.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Injections.kt deleted file mode 100644 index 232a02d7a..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Injections.kt +++ /dev/null @@ -1,375 +0,0 @@ -/* - * Lincheck - * - * Copyright (C) 2019 - 2024 JetBrains s.r.o. - * - * This Source Code Form is subject to the terms of the - * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed - * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package org.jetbrains.kotlinx.lincheck - -import java.util.* - -/** - * Methods of this object are called from the instrumented code. - */ -internal object Injections { - @JvmStatic - var lastSuspendedCancellableContinuationDuringVerification: Any? = null - - @JvmStatic - fun storeCancellableContinuation(cont: Any) { - val t = Thread.currentThread() - if (t is TestThread) { - t.suspendedContinuation = cont - } else { - lastSuspendedCancellableContinuationDuringVerification = cont - } - } - - @JvmStatic - fun enterIgnoredSection(): Boolean { - val t = Thread.currentThread() - if (t !is TestThread || t.inIgnoredSection) return false - t.inIgnoredSection = true - return true - } - - @JvmStatic - fun leaveIgnoredSection() { - val t = Thread.currentThread() as TestThread - check(t.inIgnoredSection) - t.inIgnoredSection = false - } - - @JvmStatic - fun inTestingCode(): Boolean { - val t = Thread.currentThread() - if (t !is TestThread) return false - return t.inTestingCode && !t.inIgnoredSection - } - - /** - * See [org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy.lock] for the explanation - * why we have beforeLock method. - * - * Creates a trace point which is used in the subsequent [beforeEvent] method call. - */ - @JvmStatic - fun beforeLock(codeLocation: Int) { - eventTracker.beforeLock(codeLocation) - } - - /** - * Called from instrumented code instead of the MONITORENTER instruction, but after [beforeEvent] method call, - * if the plugin is enabled. - */ - @JvmStatic - fun lock(monitor: Any) { - eventTracker.lock(monitor) - } - - /** - * Called from instrumented code instead of the MONITOREXIT instruction. - */ - @JvmStatic - fun unlock(monitor: Any, codeLocation: Int) { - eventTracker.unlock(monitor, codeLocation) - } - - /** - * Called from the instrumented code instead of `Unsafe.park`. - */ - @JvmStatic - fun park(codeLocation: Int) { - eventTracker.park(codeLocation) - } - - /** - * Called from the instrumented code instead of `Unsafe.unpark`. - */ - @JvmStatic - fun unpark(thread: Thread, codeLocation: Int) { - eventTracker.unpark(thread, codeLocation) - } - - /** - * See [org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy.wait] for the explanation - * why we have beforeWait method. - * - * Creates a trace point which is used in the subsequent [beforeEvent] method call. - */ - @JvmStatic - fun beforeWait(codeLocation: Int) { - eventTracker.beforeWait(codeLocation) - } - - /** - * Called from the instrumented code instead of [Object.wait], but after [beforeEvent] method call, - * if the plugin is enabled. - */ - @JvmStatic - fun wait(monitor: Any) { - eventTracker.wait(monitor, withTimeout = false) - } - - - /** - * Called from the instrumented code instead of [Object.wait] with timeout, but after [beforeEvent] method call, - * if the plugin is enabled. - */ - @JvmStatic - fun waitWithTimeout(monitor: Any) { - eventTracker.wait(monitor, withTimeout = true) - } - - /** - * Called from the instrumented code instead of [Object.notify]. - */ - @JvmStatic - fun notify(monitor: Any, codeLocation: Int) { - eventTracker.notify(monitor, codeLocation, notifyAll = false) - } - - /** - * Called from the instrumented code instead of [Object.notify]. - */ - @JvmStatic - fun notifyAll(monitor: Any, codeLocation: Int) { - eventTracker.notify(monitor, codeLocation, notifyAll = true) - } - - /** - * Called from the instrumented code replacing random `int` generation with a deterministic random value. - */ - @JvmStatic - fun nextInt(): Int = - eventTracker.randomNextInt() - - /** - * Called from the instrumented code to replace `ThreadLocalRandom.nextInt(origin, bound)` with a deterministic random value. - */ - @Suppress("UNUSED_PARAMETER") - @JvmStatic - fun nextInt2(origin: Int, bound: Int): Int = - eventTracker.run { - runInIgnoredSection { - getThreadLocalRandom().nextInt(bound) - } - } - - /** - * Called from the instrumented code to get a random instance that is deterministic and controlled by Lincheck. - */ - @JvmStatic - fun deterministicRandom(): Random = - eventTracker.getThreadLocalRandom() - - /** - * Called from the instrumented code to check whether the object is a [Random] instance. - */ - @JvmStatic - fun isRandom(any: Any?): Boolean = - any is Random - - /** - * Called from the instrumented code before each field read. - * - * @return whether the trace point was created - */ - @JvmStatic - fun beforeReadField(obj: Any?, className: String, fieldName: String, codeLocation: Int): Boolean { - if (obj == null) return false // Ignore, NullPointerException will be thrown - return eventTracker.beforeReadField(obj, className, fieldName, codeLocation) - } - - /** - * Called from the instrumented code before any static field read. - */ - @JvmStatic - fun beforeReadFieldStatic(className: String, fieldName: String, codeLocation: Int) { - eventTracker.beforeReadFieldStatic(className, fieldName, codeLocation) - } - - /** - * Called from the instrumented code before any array cell read. - * - * @return whether the trace point was created - */ - @JvmStatic - fun beforeReadArray(array: Any?, index: Int, codeLocation: Int): Boolean { - if (array == null) return false // Ignore, NullPointerException will be thrown - return eventTracker.beforeReadArrayElement(array, index, codeLocation) - } - - /** - * Called from the instrumented code after each field read (final field reads can be ignored here). - */ - @JvmStatic - fun afterRead(value: Any?) { - eventTracker.afterRead(value) - } - - /** - * Called from the instrumented code before each field write. - * - * @return whether the trace point was created - */ - @JvmStatic - fun beforeWriteField(obj: Any?, className: String, fieldName: String, value: Any?, codeLocation: Int): Boolean { - if (obj == null) return false // Ignore, NullPointerException will be thrown - return eventTracker.beforeWriteField(obj, className, fieldName, value, codeLocation) - } - - /** - * Called from the instrumented code before any static field write. - */ - @JvmStatic - fun beforeWriteFieldStatic(className: String, fieldName: String, value: Any?, codeLocation: Int) { - eventTracker.beforeWriteFieldStatic(className, fieldName, value, codeLocation) - } - - /** - * Called from the instrumented code before any array cell write. - * - * @return whether the trace point was created - */ - @JvmStatic - fun beforeWriteArray(array: Any?, index: Int, value: Any?, codeLocation: Int): Boolean { - if (array == null) return false // Ignore, NullPointerException will be thrown - return eventTracker.beforeWriteArrayElement(array, index, value, codeLocation) - } - - /** - * Called from the instrumented code before any write operation. - */ - @JvmStatic - fun afterWrite() { - eventTracker.afterWrite() - } - - /** - * Called from the instrumented code before any method call. - * - * @param owner is `null` for static methods. - */ - @JvmStatic - fun beforeMethodCall(owner: Any?, className: String, methodName: String, codeLocation: Int, params: Array) { - eventTracker.beforeMethodCall(owner, className, methodName, codeLocation, params) - } - - /** - * Called from the instrumented code before any atomic method call. - * This is just an optimization of [beforeMethodCall] for trusted - * atomic constructs to avoid wrapping the invocations into try-finally blocks. - */ - @JvmStatic - fun beforeAtomicMethodCall(owner: Any?, methodName: String, codeLocation: Int, params: Array) { - eventTracker.beforeAtomicMethodCall(owner, methodName, codeLocation, params) - } - - /** - * Called from the instrumented code after any method successful call, i.e., without any exception. - */ - @JvmStatic - fun onMethodCallFinishedSuccessfully(result: Any?) { - eventTracker.onMethodCallFinishedSuccessfully(result) - } - - /** - * Called from the instrumented code after any method that returns void successful call, i.e., without any exception. - */ - @JvmStatic - fun onMethodCallVoidFinishedSuccessfully() { - eventTracker.onMethodCallFinishedSuccessfully(VOID_RESULT) - } - - /** - * Called from the instrumented code after any method call threw an exception - */ - @JvmStatic - fun onMethodCallThrewException(t: Throwable) { - eventTracker.onMethodCallThrewException(t) - } - - /** - * Called from the instrumented code after any object is created - */ - @JvmStatic - fun onNewObjectCreation(obj: Any) { - eventTracker.onNewObjectCreation(obj) - } - - /** - * Called from the instrumented code after value assigned to any receiver field. - * Required to track local objects. - * @param receiver the object in whose field the entry is made - * @param value the value written into [receiver] field - * - * @see [LocalObjectManager] - */ - @JvmStatic - fun onWriteToObjectFieldOrArrayCell(obj: Any, fieldOrArrayCellValue: Any?) { - eventTracker.onWriteToObjectFieldOrArrayCell(obj, fieldOrArrayCellValue) - } - - /** - * Called from the instrumented code to replace [java.lang.Object.hashCode] method call with some - * deterministic value. - */ - @JvmStatic - internal fun hashCodeDeterministic(obj: Any): Int { - val hashCode = obj.hashCode() - // This is a dirty hack to determine whether there is a - // custom hashCode() implementation or it is always delegated - // to System.identityHashCode(..). - // While this code is not robust in theory, it works - // fine in practice. - return if (hashCode == System.identityHashCode(obj)) { - identityHashCodeDeterministic(obj) - } else { - hashCode - } - } - - /** - * Called from the instrumented code to replace [java.lang.System.identityHashCode] method call with some - * deterministic value. - */ - @JvmStatic - internal fun identityHashCodeDeterministic(obj: Any?): Int { - if (obj == null) return 0 - // TODO: easier to support when `javaagent` is merged - return 0 - } - - @JvmStatic - private val eventTracker: EventTracker - get() = (Thread.currentThread() as TestThread).eventTracker!! // should be non-null - - @JvmStatic - val VOID_RESULT = Any() - - // == Methods required for the IDEA Plugin integration == - - @JvmStatic - fun shouldInvokeBeforeEvent(): Boolean { - return eventTracker.shouldInvokeBeforeEvent() - } - - /** - * @param type type of the next event. Used only for debug purposes. - */ - @Suppress("UNUSED_PARAMETER") // for debug - @JvmStatic - fun getNextEventId(type: String): Int { - return eventTracker.getEventId() - } - - @JvmStatic - fun setLastMethodCallEventId() { - eventTracker.setLastMethodCallEventId() - } -} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt index 96a12d9f4..82b10a0a2 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt @@ -12,6 +12,7 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.transformation.withLincheckJavaAgent import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingCTestConfiguration import org.jetbrains.kotlinx.lincheck.verifier.* import kotlin.reflect.* @@ -47,12 +48,15 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) { /** * @return TestReport with information about concurrent test run. */ + @Synchronized // never run Lincheck tests in parallel internal fun checkImpl(): LincheckFailure? { check(testConfigurations.isNotEmpty()) { "No Lincheck test configuration to run" } lincheckVerificationStarted() for (testCfg in testConfigurations) { - val failure = testCfg.checkImpl() - if (failure != null) return failure + withLincheckJavaAgent(testCfg.instrumentationMode) { + val failure = testCfg.checkImpl() + if (failure != null) return failure + } } return null } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LincheckClassLoader.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/LincheckClassLoader.java deleted file mode 100644 index a07f50a77..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LincheckClassLoader.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Lincheck - * - * Copyright (C) 2019 - 2023 JetBrains s.r.o. - * - * This Source Code Form is subject to the terms of the - * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed - * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package org.jetbrains.kotlinx.lincheck; - -import org.jetbrains.kotlinx.lincheck.runner.Runner; -import org.jetbrains.kotlinx.lincheck.runner.TestThreadExecution; -import org.jetbrains.kotlinx.lincheck.strategy.Strategy; -import org.jetbrains.kotlinx.lincheck.strategy.managed.JavaUtilRemapper; -import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassVisitor; -import org.jetbrains.kotlinx.lincheck.transformation.TransformationMode; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.commons.Remapper; - -import java.io.IOException; -import java.net.URL; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static org.jetbrains.kotlinx.lincheck.LincheckClassLoader.ASM_API; -import static org.jetbrains.kotlinx.lincheck.LincheckClassLoader.REMAPPED_PACKAGE_INTERNAL_NAME; -import static org.jetbrains.kotlinx.lincheck.UtilsKt.getCanonicalClassName; -import static org.jetbrains.kotlinx.lincheck.transformation.TransformationMode.MODEL_CHECKING; -import static org.objectweb.asm.Opcodes.ASM9; -import static org.objectweb.asm.Opcodes.V1_6; - -/** - * This transformer applies required for {@link Strategy} and {@link Runner} - * class transformations and hines them from others. - */ -public class LincheckClassLoader extends ClassLoader { - public static final String REMAPPED_PACKAGE_INTERNAL_NAME = "org/jetbrains/kotlinx/lincheck/tran$f*rmed/"; - public static final String REMAPPED_PACKAGE_CANONICAL_NAME = getCanonicalClassName(REMAPPED_PACKAGE_INTERNAL_NAME); - public static final int ASM_API = ASM9; - - private final TransformationMode transformationMode; - private final Remapper remapper; - - // Cache for classloading and frames computing during the transformation. - private final Map> cache = new ConcurrentHashMap<>(); - - // Shares transformed bytecode between iterations. - private static final Map stressAndVerificationBytecodeCache = new ConcurrentHashMap<>(); - private static final Map modelCheckingBytecodeCache = new ConcurrentHashMap<>(); - - public LincheckClassLoader(TransformationMode transformationMode) { - this.transformationMode = transformationMode; - remapper = transformationMode == MODEL_CHECKING ? new JavaUtilRemapper() : null; - } - - public Map getBytecodeCache() { - if (transformationMode == MODEL_CHECKING) { - return modelCheckingBytecodeCache; - } else { - return stressAndVerificationBytecodeCache; - } - } - - public Class defineClass(String className, byte[] bytecode) { - //noinspection unchecked - return (Class) super.defineClass(className, bytecode, 0, bytecode.length); - } - - /** - * Returns `true` if the specified class should not be transformed. - */ - private static boolean doNotTransform(String className) { - if (className.startsWith(REMAPPED_PACKAGE_CANONICAL_NAME)) return false; - return className.startsWith("sun.") || - className.startsWith("java.") || - className.startsWith("jdk.internal.") || - (className.startsWith("kotlin.") && - !className.startsWith("kotlin.collections.") && // transform kotlin collections - !(className.startsWith("kotlin.jvm.internal.Array") && className.contains("Iterator")) && // transform kotlin iterator classes - !className.startsWith("kotlin.ranges.") && // transform kotlin ranges - !className.equals("kotlin.random.Random$Default") // transform Random.nextInt(..) and similar calls - ) || - className.startsWith("com.intellij.rt.coverage.") || - className.startsWith("org.jetbrains.kotlinx.lincheck.") || - className.equals(kotlinx.coroutines.DebugKt.class.getName()) || - className.equals(kotlin.coroutines.CoroutineContext.class.getName()) || - className.equals(kotlinx.coroutines.CoroutineContextKt.class.getName()) || - className.equals(kotlinx.coroutines.CoroutineScope.class.getName()) || - className.equals(kotlinx.coroutines.CancellableContinuation.class.getName()) || - className.equals(kotlinx.coroutines.CoroutineExceptionHandler.class.getName()) || - className.equals(kotlinx.coroutines.CoroutineDispatcher.class.getName()) || - className.equals("kotlinx.coroutines.NotCompleted") || - className.equals("kotlinx.coroutines.CancelHandler") || - className.equals("kotlinx.coroutines.CancelHandlerBase"); - } - - /** - * Returns `true` if the specified class should not be transformed. - * Note that this method takes into consideration whether class name will be remapped. - */ - boolean shouldBeTransformed(Class clazz) { - return !doNotTransform(remapClassName(clazz.getName())); - } - - /** - * Remaps class name if needed for transformation - */ - public String remapClassName(String className) { - if (remapper != null) { - String internalName = className.replace('.', '/'); - String remappedInternalName = remapper.mapType(internalName); - return remappedInternalName.replace('/', '.'); - } - return className; - } - - @Override - public Class loadClass(String name) throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - Class result = cache.get(name); - if (result != null) { - return result; - } - if (doNotTransform(name)) { - result = super.loadClass(name); - cache.put(name, result); - return result; - } - try { - byte[] bytes = getBytecodeCache().get(name); - if (bytes == null) { - bytes = instrument(originalName(name)); - getBytecodeCache().put(name, bytes); - } - result = defineClass(name, bytes, 0, bytes.length); - cache.put(name, result); - return result; - } catch (Exception e) { - throw new IllegalStateException("Cannot transform class " + name, e); - } - } - } - - /** - * Reads class as resource, instruments it (applies {@link Strategy}'s transformer at first, - * then {@link Runner}'s) and returns the resulting byte-code. - * - * @param className name of the class to be transformed. - * @return the byte-code of the transformed class. - * @throws IOException if class could not be read as a resource. - */ - private byte[] instrument(String className) throws IOException { - ClassReader cr = new ClassReader(className); - ClassVersionGetter infoGetter = new ClassVersionGetter(); - cr.accept(infoGetter, 0); - ClassWriter cw = new TransformationClassWriter(infoGetter.getClassVersion(), remapper); - LincheckClassVisitor cv = new LincheckClassVisitor(transformationMode, cw); - cr.accept(cv, ClassReader.EXPAND_FRAMES); - return cw.toByteArray(); - } - - /** - * Returns the original name of the specified class before transformation. - */ - private String originalName(String className) { - if (className.startsWith(REMAPPED_PACKAGE_CANONICAL_NAME)) - return className.substring(REMAPPED_PACKAGE_CANONICAL_NAME.length()); - return className; - } - - @Override - public URL getResource(String name) { - return super.getResource(name); - } -} - -/** - * ClassWriter for the classes transformed by *lincheck* with a correct - * {@link ClassWriter#getCommonSuperClass} implementation. - */ -class TransformationClassWriter extends ClassWriter { - private final Remapper remapper; - - public TransformationClassWriter(int classVersion, Remapper remapper) { - super(classVersion > V1_6 ? COMPUTE_FRAMES : COMPUTE_MAXS); - this.remapper = remapper; - } - - /** - * ASM uses `Class.forName` for given types, however it can lead to cyclic dependencies when loading transformed classes. - * Thus, we call the original method for non-transformed class names and then just fix it if needed. - */ - @Override - protected String getCommonSuperClass(final String type1, final String type2) { - String result = super.getCommonSuperClass(originalInternalName(type1), originalInternalName(type2)); - if (remapper != null) - return remapper.map(result); - return result; - } - - /** - * Returns the name of the specified class before it was transformed. - *

- * Classes from `java.util` package are moved to [TRANSFORMED_PACKAGE_NAME] during transformation, - * this method changes the package to the original one. - */ - private String originalInternalName(String internalName) { - if (internalName.startsWith(REMAPPED_PACKAGE_INTERNAL_NAME)) - return internalName.substring(REMAPPED_PACKAGE_INTERNAL_NAME.length()); - return internalName; - } -} - -/** - * Visitor for retrieving information of class version needed for making a choice between COMPUTE_FRAMES and COMPUTE_MAXS. - * COMPUTE_FRAMES implies COMPUTE_MAXS, but is more expensive, so it is used only for classes with version 1.7 or higher. - */ -class ClassVersionGetter extends ClassVisitor { - private int classVersion; - - public ClassVersionGetter() { - super(ASM_API); - } - - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - this.classVersion = version; - } - - public int getClassVersion() { - return classVersion; - } -} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt index 547e5d639..1c9a89615 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt @@ -20,7 +20,6 @@ package org.jetbrains.kotlinx.lincheck -import org.jetbrains.kotlinx.lincheck.LincheckClassLoader.REMAPPED_PACKAGE_CANONICAL_NAME import org.jetbrains.kotlinx.lincheck.strategy.managed.getObjectNumber import sun.misc.Unsafe import java.lang.reflect.Field @@ -146,10 +145,10 @@ private inline fun readFieldViaUnsafe(obj: Any?, field: Field, extractMethod private fun isAtomic(value: Any?): Boolean { if (value == null) return false return value.javaClass.canonicalName.let { - it == REMAPPED_PACKAGE_CANONICAL_NAME + "java.util.concurrent.atomic.AtomicInteger" || - it == REMAPPED_PACKAGE_CANONICAL_NAME + "java.util.concurrent.atomic.AtomicLong" || - it == REMAPPED_PACKAGE_CANONICAL_NAME + "java.util.concurrent.atomic.AtomicReference" || - it == REMAPPED_PACKAGE_CANONICAL_NAME + "java.util.concurrent.atomic.AtomicBoolean" + it == "java.util.concurrent.atomic.AtomicInteger" || + it == "java.util.concurrent.atomic.AtomicLong" || + it == "java.util.concurrent.atomic.AtomicReference" || + it == "java.util.concurrent.atomic.AtomicBoolean" } } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt index 8759cd0b0..3b8150542 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt @@ -11,6 +11,7 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.LoggingLevel.* +import sun.nio.ch.lincheck.TestThread import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* @@ -575,7 +576,7 @@ private fun StringBuilder.appendDeadlockWithDumpFailure(failure: DeadlockOrLivel appendLine("Thread-$threadNumber:") stackTrace.map { StackTraceElement( - /* declaringClass = */ it.className.removePrefix(LincheckClassLoader.REMAPPED_PACKAGE_CANONICAL_NAME), + /* declaringClass = */ it.className, /* methodName = */ it.methodName, /* fileName = */ it.fileName, /* lineNumber = */ it.lineNumber diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt index 0a765f996..ed704b4ac 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt @@ -92,7 +92,7 @@ class ExceptionResult private constructor( @Suppress("UNCHECKED_CAST") @JvmOverloads fun create(throwable: Throwable, wasSuspended: Boolean = false) = - ExceptionResult(throwable, wasSuspended, throwable::class.java.normalize()) + ExceptionResult(throwable, wasSuspended, throwable::class.java) } } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt index bf0487a6e..fe01c62ce 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt @@ -10,11 +10,12 @@ package org.jetbrains.kotlinx.lincheck import kotlinx.coroutines.* -import org.jetbrains.kotlinx.lincheck.execution.* +import sun.nio.ch.lincheck.EventTracker +import sun.nio.ch.lincheck.TestThread import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* +import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer import org.jetbrains.kotlinx.lincheck.verifier.* -import java.io.* import java.lang.ref.* import java.lang.reflect.* import java.lang.reflect.Method @@ -38,7 +39,6 @@ internal fun executeActor( try { val m = getMethod(instance, actor.method) val args = (if (actor.isSuspendable) actor.arguments + completion else actor.arguments) - .map { it.convertForLoader(instance.javaClass.classLoader) } val res = m.invoke(instance, *args.toTypedArray()) return if (m.returnType.isAssignableFrom(Void.TYPE)) VoidResult else createLincheckResult(res) } catch (invE: Throwable) { @@ -60,9 +60,6 @@ internal fun executeActor( } } -@Suppress("UNCHECKED_CAST") -internal fun Class.normalize() = LinChecker::class.java.classLoader.loadClass(name) as Class - private val methodsCache = WeakHashMap, WeakHashMap>>() /** @@ -79,7 +76,7 @@ internal fun getMethod(instance: Any, method: Method): Method { } /** - * Finds a method withe the specified [name] and (parameters)[parameterTypes] + * Finds a method with the specified [name] and (parameters)[parameterTypes] * ignoring the difference in class loaders for these parameter types. */ private fun Class.getMethod(name: String, parameterTypes: Array>): Method = @@ -94,7 +91,7 @@ private fun Class.getMethod(name: String, parameterTypes: Array ExceptionResult.create(res, wasSuspended) res === COROUTINE_SUSPENDED -> Suspended res is kotlin.Result -> res.toLinCheckResult(wasSuspended) - else -> ValueResult(res.convertForLoader(LinChecker::class.java.classLoader), wasSuspended) + else -> ValueResult(res, wasSuspended) } private fun kotlin.Result.toLinCheckResult(wasSuspended: Boolean) = @@ -114,7 +111,7 @@ private fun kotlin.Result.toLinCheckResult(wasSuspended: Boolean) = is Unit -> if (wasSuspended) SuspendedVoidResult else VoidResult // Throwable was returned as a successful result is Throwable -> ValueResult(value::class.java, wasSuspended) - else -> ValueResult(value.convertForLoader(LinChecker::class.java.classLoader), wasSuspended) + else -> ValueResult(value, wasSuspended) } } else ExceptionResult.create(exceptionOrNull()!!, wasSuspended) @@ -173,104 +170,6 @@ fun kotlin.Result.cancelledByLincheck() = exceptionOrNull() === cancellat private val cancellationByLincheckException = Exception("Cancelled by lincheck") -internal fun ExecutionScenario.convertForLoader(loader: ClassLoader) = ExecutionScenario( - initExecution.map { - it.convertForLoader(loader) - }, - parallelExecution.map { actors -> - actors.map { a -> - a.convertForLoader(loader) - } - }, - postExecution.map { - it.convertForLoader(loader) - }, - validationFunction = validationFunction -) - -private fun Actor.convertForLoader(loader: ClassLoader): Actor { - val args = arguments.map { it.convertForLoader(loader) } - // the original `isSuspendable` is used here since `KFunction.isSuspend` fails on transformed classes - return Actor( - method = method.convertForLoader(loader), - arguments = args, - cancelOnSuspension = cancelOnSuspension, - allowExtraSuspension = allowExtraSuspension, - blocking = blocking, - causesBlocking = causesBlocking, - promptCancellation = promptCancellation, - isSuspendable = isSuspendable - ) -} - -internal fun ExecutionResult.convertForLoader(loader: ClassLoader) = ExecutionResult( - initResults.map { it.convertForLoader(loader) }, - afterInitStateRepresentation, - parallelResultsWithClock.map { results -> results.map { it.convertForLoader(loader) } }, - afterParallelStateRepresentation, - postResults.map { it.convertForLoader(loader) }, - afterPostStateRepresentation -) - -/** - * Finds the same method but loaded by the specified (class loader)[loader], - * the signature can be changed according to the [LincheckClassLoader]'s remapper. - */ -private fun Method.convertForLoader(loader: ClassLoader): Method { - if (loader !is LincheckClassLoader) return this - val clazz = declaringClass.convertForLoader(loader) - val parameterTypes = parameterTypes.map { it.convertForLoader(loader) } - return clazz.getDeclaredMethod(name, *parameterTypes.toTypedArray()) -} - -private fun Class<*>.convertForLoader(loader: LincheckClassLoader): Class<*> = - if (isPrimitive) this else loader.loadClass(loader.remapClassName(name)) - -private fun ResultWithClock.convertForLoader(loader: ClassLoader): ResultWithClock = - ResultWithClock(result.convertForLoader(loader), clockOnStart) - -private fun Result.convertForLoader(loader: ClassLoader): Result = when (this) { - is ValueResult -> ValueResult(value.convertForLoader(loader), wasSuspended) - else -> this // does not need to be transformed -} - -/** - * Move the value from its current class loader to the specified [loader]. - * For primitive values, does nothing. - * Non-primitive values need to be [Serializable] for this to succeed. - */ -internal fun Any?.convertForLoader(loader: ClassLoader) = when { - this == null -> null - this::class.java.classLoader == null -> this // primitive class, no need to convert - this::class.java.classLoader == loader -> this // already in this loader - loader is LincheckClassLoader && !loader.shouldBeTransformed(this.javaClass) -> this - this is Serializable -> serialize().run { deserialize(loader) } - else -> error("The result class should either be always loaded by the system class loader and not be transformed," + - " or implement Serializable interface.") -} - -internal fun Any?.serialize(): ByteArray = ByteArrayOutputStream().use { - val oos = ObjectOutputStream(it) - oos.writeObject(this) - it.toByteArray() -} - -internal fun ByteArray.deserialize(loader: ClassLoader) = ByteArrayInputStream(this).use { - CustomObjectInputStream(loader, it).run { readObject() } -} - -/** - * ObjectInputStream that uses custom class loader. - */ -private class CustomObjectInputStream(val loader: ClassLoader, inputStream: InputStream) : ObjectInputStream(inputStream) { - override fun resolveClass(desc: ObjectStreamClass): Class<*> { - // add `TRANSFORMED_PACKAGE_NAME` prefix in case of TransformationClassLoader and remove otherwise - val className = if (loader is LincheckClassLoader) loader.remapClassName(desc.name) - else desc.name.removePrefix(LincheckClassLoader.REMAPPED_PACKAGE_CANONICAL_NAME) - return Class.forName(className, true, loader) - } -} - /** * Collects the current thread dump and keeps only those * threads that are related to the specified [runner]. @@ -280,34 +179,13 @@ internal fun collectThreadDump(runner: Runner) = Thread.getAllStackTraces().filt } internal val String.canonicalClassName get() = this.replace('/', '.') -internal val String.internalClassName get() = this.replace('.', '/') internal fun exceptionCanBeValidExecutionResult(exception: Throwable): Boolean { return exception !is ThreadDeath && // used to stop thread in FixedActiveThreadsExecutor by calling thread.stop method exception !is InternalLincheckTestUnexpectedException && - exception !is ForcibleExecutionFinishError && - !isIllegalAccessOfUnsafeDueToJavaVersion(exception) + exception !is ForcibleExecutionFinishError } -internal fun wrapInvalidAccessFromUnnamedModuleExceptionWithDescription(throwable: Throwable): Throwable { - if (isIllegalAccessOfUnsafeDueToJavaVersion(throwable)) return RuntimeException(ADD_OPENS_MESSAGE, throwable) - - return throwable -} - -internal fun isIllegalAccessOfUnsafeDueToJavaVersion(exception: Throwable): Boolean { - return exception is IllegalAccessException && exception.message?.contains("to unnamed module") ?: false -} - - -internal const val ADD_OPENS_MESSAGE = - "It seems that you use Java 9+ and the code uses Unsafe or similar constructions that are not accessible from unnamed modules.\n" + - "Please add the following lines to your test running configuration:\n" + - "--add-opens java.base/java.lang=ALL-UNNAMED\n" + - "--add-opens java.base/jdk.internal.misc=ALL-UNNAMED\n" + - "--add-exports java.base/jdk.internal.util=ALL-UNNAMED\n" + - "--add-exports java.base/sun.security.action=ALL-UNNAMED" - /** * Utility exception for test purposes. * When this exception is thrown by an operation, it will halt testing with [UnexpectedExceptionInvocationResult]. @@ -324,7 +202,14 @@ internal class LincheckInternalBugException(cause: Throwable): Exception(cause) @Suppress("UnusedReceiverParameter") internal inline fun EventTracker.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) @Suppress("UnusedReceiverParameter") +internal inline fun FixedActiveThreadsExecutor.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) +@Suppress("UnusedReceiverParameter") internal inline fun ParallelThreadsRunner.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) +@Suppress("UnusedReceiverParameter") +internal inline fun LincheckClassFileTransformer.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) + +@Suppress("UnusedReceiverParameter") +internal inline fun ExecutionClassLoader.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) private inline fun runInIgnoredSection(currentThread: Thread, block: () -> R): R = if (currentThread is TestThread && !currentThread.inIgnoredSection) { diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt index b65b1746d..081758bd4 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt @@ -13,7 +13,7 @@ import kotlinx.atomicfu.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.util.* -import org.jetbrains.kotlinx.lincheck.TestThread +import sun.nio.ch.lincheck.TestThread import java.io.* import java.lang.* import java.util.concurrent.* @@ -163,19 +163,23 @@ internal class FixedActiveThreadsExecutor(private val testName: String, private private fun testThreadRunnable(iThread: Int) = Runnable { loop@ while (true) { - val task = getTask(iThread) - if (task === Shutdown) return@Runnable - tasks[iThread].value = null // reset task - val threadExecution = task as TestThreadExecution - check(threadExecution.iThread == iThread) + // TODO: do we need the added ignored section here? We should be not in the testing code. + val task = runInIgnoredSection { + val task = getTask(iThread) + if (task === Shutdown) return@Runnable + tasks[iThread].value = null // reset task + task as TestThreadExecution + } + check(task.iThread == iThread) try { - threadExecution.run() + task.run() } catch(e: Throwable) { - val wrapped = wrapInvalidAccessFromUnnamedModuleExceptionWithDescription(e) - setResult(iThread, wrapped) + // TODO: do we need the added ignored section here? We should be not in the testing code. + runInIgnoredSection { setResult(iThread, e) } continue@loop } - setResult(iThread, Done) + // TODO: do we need the added ignored section here? We should be not in the testing code. + runInIgnoredSection { setResult(iThread, Done) } } } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt index 2e3abdc3a..fdec47ea2 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt @@ -18,8 +18,10 @@ import org.jetbrains.kotlinx.lincheck.runner.ParallelThreadsRunner.Completion.* import org.jetbrains.kotlinx.lincheck.runner.UseClocks.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy -import org.jetbrains.kotlinx.lincheck.util.SpinnerGroup -import org.jetbrains.kotlinx.lincheck.TestThread +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingStrategy +import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent +import org.jetbrains.kotlinx.lincheck.util.* +import sun.nio.ch.lincheck.* import java.lang.reflect.* import java.util.concurrent.* import java.util.concurrent.atomic.* @@ -101,7 +103,9 @@ internal open class ParallelThreadsRunner( override var context = ParallelThreadRunnerInterceptor(resWithCont) + StoreExceptionHandler() + Job() - override fun resumeWith(result: kotlin.Result) { + // We need to run this code in an ignored section, + // as it is called in the testing code but should not be analyzed. + override fun resumeWith(result: kotlin.Result) = runInIgnoredSection { // decrement completed or suspended threads only if the operation was not cancelled and // the continuation was not intercepted; it was already decremented before writing `resWithCont` otherwise if (!result.cancelledByLincheck()) { @@ -131,7 +135,10 @@ internal open class ParallelThreadsRunner( private inner class ParallelThreadRunnerInterceptor( private var resWithCont: SuspensionPointResultWithContinuation ) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { - override fun interceptContinuation(continuation: Continuation): Continuation { + + // We need to run this code in an ignored section, + // as it is called in the testing code but should not be analyzed. + override fun interceptContinuation(continuation: Continuation): Continuation = runInIgnoredSection { return Continuation(StoreExceptionHandler() + Job()) { result -> // decrement completed or suspended threads only if the operation was not cancelled if (!result.cancelledByLincheck()) { @@ -170,8 +177,17 @@ internal open class ParallelThreadsRunner( validationPartExecution?.results?.fill(null) } + private var ensuredTestInstanceIsTransformed = false + private fun createTestInstance() { testInstance = testClass.newInstance() + // In the model checking mode, we need to ensure + // that all the necessary classes and instrumented + // after creating a test instance. + if (strategy is ModelCheckingStrategy && !ensuredTestInstanceIsTransformed) { + LincheckJavaAgent.ensureObjectIsTransformed(testInstance) + ensuredTestInstanceIsTransformed = true + } testThreadExecutions.forEach { it.testInstance = testInstance } validationPartExecution?.let { it.testInstance = testInstance } } @@ -206,7 +222,9 @@ internal open class ParallelThreadsRunner( override fun afterCoroutineCancelled(iThread: Int) {} - private fun waitAndInvokeFollowUp(iThread: Int, actorId: Int): Result { + // We need to run this code in an ignored section, + // as it is called in the testing code but should not be analyzed. + private fun waitAndInvokeFollowUp(iThread: Int, actorId: Int): Result = runInIgnoredSection { // Coroutine is suspended. Call method so that strategy can learn it. afterCoroutineSuspended(iThread) // If the suspended method call has a follow-up part after this suspension point, @@ -305,7 +323,7 @@ internal open class ParallelThreadsRunner( afterInitStateRepresentation = afterInitStateRepresentation, afterParallelStateRepresentation = afterParallelStateRepresentation, afterPostStateRepresentation = afterPostStateRepresentation - ).convertForLoader(LinChecker::class.java.classLoader) + ) ) } catch (e: TimeoutException) { val threadDump = collectThreadDump(this) @@ -394,7 +412,7 @@ internal open class ParallelThreadsRunner( } override fun constructStateRepresentation() = - stateRepresentationFunction?.let { getMethod(testInstance, it) }?.invoke(testInstance) as String? + stateRepresentationFunction?.invoke(testInstance) as String? override fun close() { super.close() diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt index 6086d5b8d..b4fb3453f 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt @@ -13,30 +13,18 @@ import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.strategy.* -import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* -import org.jetbrains.kotlinx.lincheck.transformation.* -import org.jetbrains.kotlinx.lincheck.transformation.TransformationMode.* -import org.objectweb.asm.* import java.io.* import java.lang.reflect.* import java.util.concurrent.atomic.* -/** - * Runner determines how to run your concurrent test. In order to support techniques - * like fibers, it may require code transformation, so that [createTransformer] should - * provide the corresponding transformer and [needsTransformation] should return `true`. - */ abstract class Runner protected constructor( protected val strategy: Strategy, - private val _testClass: Class<*>, // will be transformed later + protected val testClass: Class<*>, protected val validationFunction: Actor?, protected val stateRepresentationFunction: Method? ) : Closeable { - val classLoader = LincheckClassLoader( - /* transformationMode = */ if (strategy is ModelCheckingStrategy) MODEL_CHECKING else STRESS - ) - protected val scenario: ExecutionScenario = strategy.scenario.convertForLoader(classLoader) - protected val testClass: Class<*> = classLoader.loadClass(_testClass.typeName) + val classLoader = ExecutionClassLoader() + protected val scenario: ExecutionScenario = strategy.scenario protected val completedOrSuspendedThreads = AtomicInteger(0) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/AtomicFieldUpdaterNames.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt similarity index 74% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/AtomicFieldUpdaterNames.kt rename to src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt index aea48d2ec..aecef04d3 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/AtomicFieldUpdaterNames.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt @@ -8,9 +8,9 @@ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.jetbrains.kotlinx.lincheck +package org.jetbrains.kotlinx.lincheck.strategy.managed -import sun.misc.Unsafe +import org.jetbrains.kotlinx.lincheck.UnsafeHolder.UNSAFE import java.lang.reflect.Modifier import java.util.concurrent.atomic.AtomicIntegerFieldUpdater import java.util.concurrent.atomic.AtomicLongFieldUpdater @@ -22,15 +22,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater * equality and does not prevent them from being garbage collected. */ internal object AtomicFieldUpdaterNames { - private val unsafe: Unsafe = try { - val unsafeField = Unsafe::class.java.getDeclaredField("theUnsafe") - unsafeField.isAccessible = true - unsafeField.get(null) as Unsafe - } catch (ex: Exception) { - throw RuntimeException("Can't get the Unsafe instance, please report it to the Lincheck team", ex) - } - - fun getName(updater: Any): String? { + fun getAtomicFieldUpdaterName(updater: Any): String? { if (updater !is AtomicIntegerFieldUpdater<*> && updater !is AtomicLongFieldUpdater<*> && updater !is AtomicReferenceFieldUpdater<*, *>) { throw IllegalArgumentException("Provided object is not a recognized Atomic*FieldUpdater type.") } @@ -38,16 +30,16 @@ internal object AtomicFieldUpdaterNames { try { // Cannot use neither reflection not MethodHandles.Lookup, as they lead to a warning. val tclassField = updater.javaClass.getDeclaredField("tclass") - val targetType = unsafe.getObject(updater, unsafe.objectFieldOffset(tclassField)) as Class<*> + val targetType = UNSAFE.getObject(updater, UNSAFE.objectFieldOffset(tclassField)) as Class<*> val offsetField = updater.javaClass.getDeclaredField("offset") - val offset = unsafe.getLong(updater, unsafe.objectFieldOffset(offsetField)) + val offset = UNSAFE.getLong(updater, UNSAFE.objectFieldOffset(offsetField)) for (field in targetType.declaredFields) { try { if (Modifier.isNative(field.modifiers)) continue - val fieldOffset = if (Modifier.isStatic(field.modifiers)) unsafe.staticFieldOffset(field) - else unsafe.objectFieldOffset(field) + val fieldOffset = if (Modifier.isStatic(field.modifiers)) UNSAFE.staticFieldOffset(field) + else UNSAFE.objectFieldOffset(field) if (fieldOffset == offset) return field.name } catch (t: Throwable) { t.printStackTrace() diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/JavaUtilRemapper.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/JavaUtilRemapper.kt deleted file mode 100644 index 11c1a7f48..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/JavaUtilRemapper.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Lincheck - * - * Copyright (C) 2019 - 2023 JetBrains s.r.o. - * - * This Source Code Form is subject to the terms of the - * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed - * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.jetbrains.kotlinx.lincheck.strategy.managed - -import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.transformation.NOT_TRANSFORMED_JAVA_UTIL_CLASSES -import org.jetbrains.kotlinx.lincheck.transformation.TRANSFORMED_JAVA_UTIL_INTERFACES -import org.objectweb.asm.commons.* -import org.reflections.* -import org.reflections.scanners.* - -/** - * Changes package of transformed classes from `java.util` package, excluding some, - * because transformed classes can not lie in `java.util` - */ -internal class JavaUtilRemapper : Remapper() { - override fun map(name: String): String { - if (name.startsWith("java/util/")) { - val normalizedName = name.canonicalClassName - val originalClass = ClassLoader.getSystemClassLoader().loadClass(normalizedName) - // transformation of exceptions causes a lot of trouble with catching expected exceptions - val isException = Throwable::class.java.isAssignableFrom(originalClass) - // function package is not transformed, because AFU uses it, and thus, there will be transformation problems - val inFunctionPackage = name.startsWith("java/util/function/") - // some api classes that provide low-level access can not be transformed - val isAFU = name.startsWith("java/util/concurrent/atomic/Atomic") && name.endsWith("FieldUpdater") - // interfaces are not transformed by default and are in the special set when they should be transformed - val isTransformedInterface = originalClass.isInterface && originalClass.name.internalClassName in TRANSFORMED_JAVA_UTIL_INTERFACES - // classes are transformed by default and are in the special set when they should not be transformed - val isTransformedClass = !originalClass.isInterface && originalClass.name.internalClassName !in NOT_TRANSFORMED_JAVA_UTIL_CLASSES - // no need to transform enum - val isEnum = originalClass.isEnum - if (!isAFU && !isException && !inFunctionPackage && !isEnum && (isTransformedClass || isTransformedInterface)) - return LincheckClassLoader.REMAPPED_PACKAGE_INTERNAL_NAME + name - } - return name - } -} - -/** - * Finds the transformation problems related to the `java.util` - * remapping in standard java packages. Use it for each JDK update - * to check the consistency of the related work-arounds. - */ -fun main() = listOf( - "java.util", - "java.lang", - "java.math", - "java.text", - "java.time" -).forEach { findAllTransformationProblemsIn(it) } - -/** - * Finds transformation problems because of `java.util` in the given package. - * Note that it supposes that all classes, other than `java.util` classes, are not transformed, - * because transformed classes do not cause problems. - * - * There are still some problems left, but with rarely used classes such as `java.lang.System`. - */ -private fun findAllTransformationProblemsIn(packageName: String) { - val classes = getAllClasses(packageName).map { Class.forName(it) } - for (clazz in classes) { - if (clazz.name.startsWith("java.util")) { - // skip if the class is transformed - if (clazz.isInterface && clazz.name.internalClassName in TRANSFORMED_JAVA_UTIL_INTERFACES) continue - if (!clazz.isInterface && clazz.name.internalClassName !in NOT_TRANSFORMED_JAVA_UTIL_CLASSES) continue - } - val superInterfaces = clazz.interfaces - superInterfaces.firstOrNull { it.name.internalClassName in TRANSFORMED_JAVA_UTIL_INTERFACES }?.let { - println("CONFLICT: ${clazz.name} is not transformed, but its sub-interface ${it.name} is" ) - } - clazz.superclass?.let { - if (it.causesTransformationProblem) - println("CONFLICT: ${clazz.name} is not transformed, but its subclass ${it.name} is") - } - for (f in clazz.fields) { - if (f.type.causesTransformationProblem) - println("CONFLICT: ${clazz.name} is not transformed, but its public field of type `${f.type.name}` is") - } - for (m in clazz.methods) { - if (m.returnType.causesTransformationProblem) - println("CONFLICT: ${clazz.name} is not transformed, but the return type ${m.returnType.name} in its method `${m.name}` is") - for (p in m.parameterTypes) { - if (p.causesTransformationProblem) - println("CONFLICT: ${clazz.name} is not transformed, but the parameter type ${p.name} in its method `${m.name}` is") - } - } - } -} - -private fun getAllClasses(packageName: String): List { - check(packageName != "java") { "For some reason unable to find classes in `java` package. Use more specified name such as `java.util`" } - val reflections = Reflections( - packageName.replace('.', '/'), - SubTypesScanner(false) - ) - return reflections.allTypes.toList() -} - -/** - * Checks whether class causes transformation problem if is used from a non-transformed class. - * Transformed `java.util` classes cause such problems, because they are moved to another package. - */ -private val Class<*>.causesTransformationProblem: Boolean get() = when { - !name.startsWith("java.util.") -> false - isEnum -> false - isArray -> this.componentType.causesTransformationProblem - isInterface -> name.internalClassName in TRANSFORMED_JAVA_UTIL_INTERFACES - else -> name.internalClassName !in NOT_TRANSFORMED_JAVA_UTIL_CLASSES -} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt index 22cb9a356..680ce00e1 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt @@ -12,25 +12,21 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed import kotlinx.coroutines.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.CancellationResult.* -import org.jetbrains.kotlinx.lincheck.exceptionCanBeValidExecutionResult import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart.* import org.jetbrains.kotlinx.lincheck.strategy.* -import org.jetbrains.kotlinx.lincheck.util.SpinnerGroup +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* +import org.jetbrains.kotlinx.lincheck.transformation.* +import org.jetbrains.kotlinx.lincheck.util.* import org.jetbrains.kotlinx.lincheck.verifier.* -import org.jetbrains.kotlinx.lincheck.transformation.CodeLocations -import org.jetbrains.kotlinx.lincheck.Injections -import org.jetbrains.kotlinx.lincheck.EventTracker -import org.jetbrains.kotlinx.lincheck.TestThread -import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingStrategy +import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicFieldUpdaterNames.getAtomicFieldUpdaterName +import sun.nio.ch.lincheck.* +import java.lang.invoke.* import sun.misc.Unsafe -import java.lang.invoke.VarHandle +import java.util.concurrent.atomic.* import java.lang.reflect.* import java.util.* -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater -import java.util.concurrent.atomic.AtomicLongFieldUpdater -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater import kotlin.collections.set /** @@ -51,6 +47,7 @@ abstract class ManagedStrategy( ) : Strategy(scenario), EventTracker { // The number of parallel threads. protected val nThreads: Int = scenario.nThreads + // Runner for scenario invocations, // can be replaced with a new one for trace construction. internal var runner: ManagedStrategyRunner = createRunner() @@ -62,12 +59,16 @@ abstract class ManagedStrategy( // Which thread is allowed to perform operations? @Volatile protected var currentThread: Int = 0 + // Which threads finished all the operations? private val finished = BooleanArray(nThreads) { false } + // Which threads are suspended? private val isSuspended = BooleanArray(nThreads) { false } + // Current actor id for each thread. protected val currentActorId = IntArray(nThreads) + // Detector of loops or hangs (i.e. active locks). protected val loopDetector: LoopDetector = LoopDetector(testCfg.hangingDetectionThreshold) @@ -84,28 +85,37 @@ abstract class ManagedStrategy( protected var collectTrace = false // Collector of all events in the execution such as thread switches. private var traceCollector: TraceCollector? = null // null when `collectTrace` is false + // Stores the currently executing methods call stack for each thread. private val callStackTrace = Array(nThreads) { mutableListOf() } + // Stores the global number of method calls. private var methodCallNumber = 0 + // In case of suspension, the call stack of the corresponding `suspend` // methods is stored here, so that the same method call identifiers are // used on resumption, and the trace point before and after the suspension // correspond to the same method call in the trace. private val suspendedFunctionsStack = Array(nThreads) { mutableListOf() } + // Helps to ignore potential switch point in local objects (see LocalObjectManager) to avoid // useless interleavings analysis. private var localObjectManager = LocalObjectManager() + // Last read trace point, occurred in the current thread. // We store it as we initialize read value after the point is created so we have to store // the trace point somewhere to obtain it later. private var lastReadTracePoint = Array(nThreads) { null } + // Random instances with fixed seeds to replace random calls in instrumented code. private var randoms = (0 until nThreads + 2).map { Random(it + 239L) } + // Current call stack for a thread, updated during beforeMethodCall and afterMethodCall methods. private val methodCallTracePointStack = (0 until nThreads + 2).map { mutableListOf() } + // User-specified guarantees on specific function, which can be considered as atomic or ignored. private val userDefinedGuarantees: List? = testCfg.guarantees.ifEmpty { null } + // Utility class for the plugin integration to provide ids for each trace point private var eventIdProvider = EventIdProvider() @@ -211,7 +221,6 @@ abstract class ManagedStrategy( // Re-transform class constructing trace collectTrace = true // Replace the current runner with a new one in order to use a new - // `TransformationClassLoader` with a transformer that inserts the trace collection logic. runner.close() runner = createRunner() @@ -225,7 +234,8 @@ abstract class ManagedStrategy( // Therefore, if the runner detects deadlock, we don't even try to collect trace. if (loggedResults is RunnerTimeoutInvocationResult) return null val sameResultTypes = loggedResults.javaClass == failingResult.javaClass - val sameResults = loggedResults !is CompletedInvocationResult || failingResult !is CompletedInvocationResult || loggedResults.results == failingResult.results + val sameResults = + loggedResults !is CompletedInvocationResult || failingResult !is CompletedInvocationResult || loggedResults.results == failingResult.results check(sameResultTypes && sameResults) { StringBuilder().apply { appendln("Non-determinism found. Probably caused by non-deterministic code (WeakHashMap, Object.hashCode, etc).") @@ -390,6 +400,7 @@ abstract class ManagedStrategy( loopDetector.onThreadFinish(iThread) doSwitchCurrentThread(iThread, true) } + /** * This method is executed if an illegal exception has been thrown (see [exceptionCanBeValidExecutionResult]). * @param iThread the number of the executed thread according to the [scenario][ExecutionScenario]. @@ -403,11 +414,10 @@ abstract class ManagedStrategy( // the managed strategy can construct a trace to reproduce this failure. // Let's then store the corresponding failing result and construct the trace. if (exception === ForcibleExecutionFinishError) return // not a forcible execution finish - suddenInvocationResult = - UnexpectedExceptionInvocationResult(wrapInvalidAccessFromUnnamedModuleExceptionWithDescription(exception)) + suddenInvocationResult = UnexpectedExceptionInvocationResult(exception) } - override fun onActorStart(iThread: Int) { + override fun onActorStart(iThread: Int) = runInIgnoredSection { currentActorId[iThread]++ callStackTrace[iThread].clear() suspendedFunctionsStack[iThread].clear() @@ -436,7 +446,7 @@ abstract class ManagedStrategy( * Waits until the specified thread can continue * the execution according to the strategy decision. */ - private fun awaitTurn(iThread: Int) { + private fun awaitTurn(iThread: Int) = runInIgnoredSection { spinners[iThread].spinWaitUntil { // Finish forcibly if an error occurred and we already have an `InvocationResult`. if (suddenInvocationResult != null) throw ForcibleExecutionFinishError @@ -677,7 +687,17 @@ abstract class ManagedStrategy( true } + override fun beforeReadFinalFieldStatic(className: String) = runInIgnoredSection { + // We need to ensure all the classes related to the reading object are instrumented. + // The following call checks all the static fields. + LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className.canonicalClassName) + } + override fun beforeReadFieldStatic(className: String, fieldName: String, codeLocation: Int) = runInIgnoredSection { + // We need to ensure all the classes related to the reading object are instrumented. + // The following call checks all the static fields. + LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className.canonicalClassName) + val iThread = currentThread val tracePoint = if (collectTrace) { ReadTracePoint( @@ -822,15 +842,23 @@ abstract class ManagedStrategy( } } - override fun onNewObjectCreation(obj: Any) { + override fun beforeNewObjectCreation(className: String) = runInIgnoredSection { + LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className) + } + + override fun afterNewObjectCreation(obj: Any) { if (obj is String || obj is Int || obj is Long || obj is Byte || obj is Char || obj is Float || obj is Double) return runInIgnoredSection { localObjectManager.registerNewObject(obj) } } - override fun onWriteToObjectFieldOrArrayCell(obj: Any, fieldOrArrayCellValue: Any?) { - localObjectManager.onWriteToObjectFieldOrArrayCell(obj, fieldOrArrayCellValue) + override fun onWriteToObjectFieldOrArrayCell(receiver: Any, fieldOrArrayCellValue: Any?) = runInIgnoredSection { + localObjectManager.onWriteToObjectFieldOrArrayCell(receiver, fieldOrArrayCellValue) + } + + override fun onWriteObjectToStaticField(fieldValue: Any?) = runInIgnoredSection { + localObjectManager.markObjectNonLocal(fieldValue) } private fun methodGuaranteeType(owner: Any?, className: String, methodName: String): ManagedGuaranteeType? = runInIgnoredSection { @@ -864,16 +892,31 @@ abstract class ManagedStrategy( beforeMethodCall(currentThread, codeLocation, null, methodName, params) } } + // It's important that this method can't be called inside runInIgnoredSection, as the ignored section + // flag would be set to false when leaving runInIgnoredSection, + // so enterIgnoredSection would have no effect enterIgnoredSection() } + ManagedGuaranteeType.TREAT_AS_ATOMIC -> { - if (collectTrace) { - beforeMethodCall(currentThread, codeLocation, null, methodName, params) + runInIgnoredSection { + if (collectTrace) { + beforeMethodCall(currentThread, codeLocation, null, methodName, params) + } + newSwitchPointOnAtomicMethodCall(codeLocation) } - newSwitchPointOnAtomicMethodCall(codeLocation) + // It's important that this method can't be called inside runInIgnoredSection, as the ignored section + // flag would be set to false when leaving runInIgnoredSection, + // so enterIgnoredSection would have no effect enterIgnoredSection() } + null -> { + if (owner == null) { + runInIgnoredSection { + LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className.canonicalClassName) + } + } if (collectTrace) { runInIgnoredSection { val params = if (isSuspendFunction(className, methodName, params)) { @@ -896,7 +939,7 @@ abstract class ManagedStrategy( ) = runInIgnoredSection { if (collectTrace) { val isAtomicUpdater = owner is AtomicIntegerFieldUpdater<*> || owner is AtomicLongFieldUpdater<*> || owner is AtomicReferenceFieldUpdater<*, *> - val ownerName = if (isAtomicUpdater) owner?.let { AtomicFieldUpdaterNames.getName(it) } else null + val ownerName = if (isAtomicUpdater) owner?.let { getAtomicFieldUpdaterName(it) } else null // Drop the object instance and offset (in case of Unsafe) from the parameters // when using Unsafe, VarHandle, or AtomicFieldUpdater. @Suppress("NAME_SHADOWING") @@ -1704,7 +1747,7 @@ internal class ManagedStrategyRunner( if (cancellationResult != CANCELLATION_FAILED) managedStrategy.afterCoroutineCancelled() return cancellationResult - } catch(e: Throwable) { + } catch (e: Throwable) { cancellationTracePoint?.initializeException(e) throw e // throw further } @@ -1719,10 +1762,12 @@ private class MonitorTracker(nThreads: Int) { // Maintains a set of acquired monitors with an information on which thread // performed the acquisition and the reentrancy depth. private val acquiredMonitors = IdentityHashMap() + // Maintains a set of monitors on which each thread is waiting. // Note, that a thread can wait on a free monitor if it is waiting for a `notify` call. // Stores `null` if thread is not waiting on any monitor. private val waitingMonitor = Array(nThreads) { null } + // Stores `true` for the threads which are waiting for a // `notify` call on the monitor stored in `acquiringMonitor`. private val waitForNotify = BooleanArray(nThreads) { false } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt index f9f176971..ff9756d5e 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt @@ -11,7 +11,6 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.CancellationResult.* -import org.jetbrains.kotlinx.lincheck.LincheckClassLoader.* import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart import java.math.* import kotlin.coroutines.* @@ -326,7 +325,7 @@ private val Class?.isImmutableWithNiceToString get() = this?.canonicalN kotlinx.coroutines.internal.Symbol::class.java, ).map { it.canonicalName } + listOf( - REMAPPED_PACKAGE_CANONICAL_NAME + "java.util.Collections.SingletonList", - REMAPPED_PACKAGE_CANONICAL_NAME + "java.util.Collections.SingletonMap", - REMAPPED_PACKAGE_CANONICAL_NAME + "java.util.Collections.SingletonSet" + "java.util.Collections.SingletonList", + "java.util.Collections.SingletonMap", + "java.util.Collections.SingletonSet" ) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt index 29e0397e0..244d634a5 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt @@ -13,6 +13,8 @@ import org.jetbrains.kotlinx.lincheck.Actor import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.* import org.jetbrains.kotlinx.lincheck.verifier.* import java.lang.reflect.* @@ -43,7 +45,11 @@ class ModelCheckingCTestConfiguration(testClass: Class<*>, iterations: Int, thre timeoutMs = timeoutMs, customScenarios = customScenarios ) { + + override val instrumentationMode: InstrumentationMode get() = MODEL_CHECKING + private var isReplayModeForIdeaPluginEnabled = false + override fun createStrategy(testClass: Class<*>, scenario: ExecutionScenario, validationFunction: Actor?, stateRepresentationMethod: Method?, verifier: Verifier): Strategy = ModelCheckingStrategy(this, testClass, scenario, validationFunction, stateRepresentationMethod, verifier, isReplayModeForIdeaPluginEnabled) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt index 321ed1cfd..62a289def 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt @@ -9,6 +9,7 @@ */ package org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking +import sun.nio.ch.lincheck.TestThread import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.ExceptionNumberAndStacktrace import org.jetbrains.kotlinx.lincheck.execution.* diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt index aad5a9895..a15d27aed 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt @@ -11,6 +11,8 @@ package org.jetbrains.kotlinx.lincheck.strategy.stress import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.* import org.jetbrains.kotlinx.lincheck.verifier.* import java.lang.reflect.* @@ -36,6 +38,9 @@ class StressCTestConfiguration( timeoutMs = timeoutMs, customScenarios = customScenarios ) { + + override val instrumentationMode: InstrumentationMode get() = STRESS + override fun createStrategy(testClass: Class<*>, scenario: ExecutionScenario, validationFunction: Actor?, stateRepresentationMethod: Method?, verifier: Verifier) = StressStrategy(this, testClass, scenario, validationFunction, stateRepresentationMethod, verifier) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/CodeInformation.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/CodeLocations.kt similarity index 90% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/CodeInformation.kt rename to src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/CodeLocations.kt index adcf877ac..fab94c9e0 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/CodeInformation.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/CodeLocations.kt @@ -10,7 +10,6 @@ package org.jetbrains.kotlinx.lincheck.transformation -import org.jetbrains.kotlinx.lincheck.LincheckClassLoader.* import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.FieldInfo.* import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.FinalFieldsVisitor import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.addFinalField @@ -130,20 +129,18 @@ internal object FinalFields { if (fieldName in visitor.finalFields || fieldName in visitor.mutableFields) return true // If field is not present in this class - search in the superclass recursively. visitor.superClassName?.let { internalSuperClassName -> - val internalSuperClassNameNormalized = internalSuperClassName.normalizedClassName - val superClassFields = classToFieldsMap.computeIfAbsent(internalSuperClassNameNormalized) { hashMapOf() } - val fieldFound = collectFieldInformation(internalSuperClassNameNormalized, fieldName, superClassFields) + val superClassFields = classToFieldsMap.computeIfAbsent(internalSuperClassName) { hashMapOf() } + val fieldFound = collectFieldInformation(internalSuperClassName, fieldName, superClassFields) // Copy all field information found in the superclass to the current class map. - addFieldsInfoFromSuperclass(internalSuperClassNameNormalized, internalClassName) + addFieldsInfoFromSuperclass(internalSuperClassName, internalClassName) if (fieldFound) return true } // If field is not present in this class - search in the all implemented interfaces recursively. visitor.implementedInterfaces.forEach { interfaceName -> - val normalizedInterfaceName = interfaceName.normalizedClassName - val interfaceFields = classToFieldsMap.computeIfAbsent(normalizedInterfaceName) { hashMapOf() } - val fieldFound = collectFieldInformation(normalizedInterfaceName, fieldName, interfaceFields) + val interfaceFields = classToFieldsMap.computeIfAbsent(interfaceName) { hashMapOf() } + val fieldFound = collectFieldInformation(interfaceName, fieldName, interfaceFields) // Copy all field information found in the interface to the current class map. - addFieldsInfoFromSuperclass(normalizedInterfaceName, internalClassName) + addFieldsInfoFromSuperclass(interfaceName, internalClassName) if (fieldFound) return true } // There is no such field in this class. @@ -176,15 +173,6 @@ internal object FinalFields { } } - private val String.normalizedClassName: String - get() { - var internalName = this - if (internalName.startsWith(REMAPPED_PACKAGE_INTERNAL_NAME)) { - internalName = internalName.substring(REMAPPED_PACKAGE_INTERNAL_NAME.length) - } - return internalName - } - /** * This visitor collects information about fields, declared in this class, * about superclass and implemented interfaces. diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt index e882c5a4c..4fdb624ca 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt @@ -10,26 +10,23 @@ package org.jetbrains.kotlinx.lincheck.transformation -import org.jetbrains.kotlinx.lincheck.LincheckClassLoader.ASM_API -import org.jetbrains.kotlinx.lincheck.Injections -import org.jetbrains.kotlinx.lincheck.ideaPluginEnabled -import org.jetbrains.kotlinx.lincheck.strategy.managed.JavaUtilRemapper +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.* import org.objectweb.asm.* import org.objectweb.asm.Opcodes.* import org.objectweb.asm.Type.* import org.objectweb.asm.commons.* import org.objectweb.asm.commons.InstructionAdapter.* import org.jetbrains.kotlinx.lincheck.transformation.CoroutineInternalCallTracker.isCoroutineInternalClass -import org.jetbrains.kotlinx.lincheck.transformation.TransformationMode.* +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.* +import sun.nio.ch.lincheck.* +import sun.nio.ch.lincheck.Injections.* import java.util.* internal class LincheckClassVisitor( - private val transformationMode: TransformationMode, - classVisitor: ClassVisitor, -) : ClassVisitor( - ASM_API, - if (transformationMode == MODEL_CHECKING) ClassRemapper(classVisitor, JavaUtilRemapper()) else classVisitor -) { + private val instrumentationMode: InstrumentationMode, + classVisitor: ClassVisitor +) : ClassVisitor(ASM_API, classVisitor) { private val ideaPluginEnabled = ideaPluginEnabled() private lateinit var className: String private var classVersion = 0 @@ -75,15 +72,9 @@ internal class LincheckClassVisitor( signature: String?, exceptions: Array? ): MethodVisitor { - if (access and ACC_NATIVE != 0 && methodName == "VMSupportsCS8") { - // Replace native method VMSupportsCS8 in AtomicLong with our stub. - // TODO: remove this code when javaagents are merged. - val mv = super.visitMethod(access xor ACC_NATIVE, methodName, desc, signature, exceptions) - return VMSupportsCS8MethodGenerator(GeneratorAdapter(mv, access xor ACC_NATIVE, methodName, desc)) - } var mv = super.visitMethod(access, methodName, desc, signature, exceptions) if (access and ACC_NATIVE != 0) return mv - if (transformationMode == STRESS || transformationMode == VERIFICATION) { + if (instrumentationMode == STRESS) { return if (methodName != "" && methodName != "") { CoroutineCancellabilitySupportMethodTransformer(mv, access, methodName, desc) } else { @@ -136,21 +127,6 @@ internal class LincheckClassVisitor( return mv } - /** - * Generates body of a native method `VMSupportsCS8()`. - * Native methods in java.util can not be transformed, so we should replace them with stubs. - * TODO: remove this code when javaagents are merged. - */ - private class VMSupportsCS8MethodGenerator(val adapter: GeneratorAdapter) : MethodVisitor(ASM_API, null) { - override fun visitEnd() = adapter.run { - visitCode() - push(true) // suppose that we always have CAS for Long - returnValue() - visitMaxs(1, 0) - visitEnd() - } - } - private class CoroutineCancellabilitySupportMethodTransformer( mv: MethodVisitor, access: Int, @@ -378,13 +354,18 @@ internal class LincheckClassVisitor( /** * Makes java.util.Random and all classes that extend it deterministic. * In every Random method invocation replaces the owner with Random from ManagedStateHolder. - * TODO: Kotlin's random support */ private inner class DeterministicRandomTransformer(methodName: String, adapter: GeneratorAdapter) : ManagedStrategyMethodVisitor(methodName, adapter) { override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) = adapter.run { - if (owner == "java/util/concurrent/ThreadLocalRandom" || owner == "java/util/concurrent/atomic/Striped64") { + if (owner == "java/util/concurrent/ThreadLocalRandom" || + owner == "java/util/concurrent/atomic/Striped64" || + owner == "java/util/concurrent/atomic/LongAdder" || + owner == "java/util/concurrent/atomic/DoubleAdder" || + owner == "java/util/concurrent/atomic/LongAccumulator" || + owner == "java/util/concurrent/atomic/DoubleAccumulator" + ) { if (name == "nextSecondarySeed" || name == "getProbe") { // INVOKESTATIC invokeIfInTestingCode( original = { @@ -531,11 +512,32 @@ internal class LincheckClassVisitor( lateinit var analyzer: AnalyzerAdapter override fun visitFieldInsn(opcode: Int, owner: String, fieldName: String, desc: String) = adapter.run { - if (isCoroutineInternalClass(owner) || isCoroutineStateMachineClass(owner) || FinalFields.isFinalField(owner, fieldName) - ) { + if (isCoroutineInternalClass(owner) || isCoroutineStateMachineClass(owner)) { visitFieldInsn(opcode, owner, fieldName, desc) return } + if (FinalFields.isFinalField(owner, fieldName)) { + if (opcode == GETSTATIC) { + invokeIfInTestingCode( + original = { + visitFieldInsn(opcode, owner, fieldName, desc) + }, + code = { + // STACK: + push(owner) + // STACK: className: String, fieldName: String, codeLocation: Int + invokeStatic(Injections::beforeReadFinalFieldStatic) + // STACK: owner: Object + visitFieldInsn(opcode, owner, fieldName, desc) + // STACK: value + } + ) + return + } else { + visitFieldInsn(opcode, owner, fieldName, desc) + return + } + } when (opcode) { GETSTATIC -> { invokeIfInTestingCode( @@ -1124,7 +1126,7 @@ internal class LincheckClassVisitor( storeLocal(objectLocal) visitMethodInsn(opcode, owner, name, desc, itf) loadLocal(objectLocal) - invokeStatic(Injections::onNewObjectCreation) + invokeStatic(Injections::afterNewObjectCreation) } ) } else { @@ -1139,20 +1141,29 @@ internal class LincheckClassVisitor( original = {}, code = { dup() - invokeStatic(Injections::onNewObjectCreation) + invokeStatic(Injections::afterNewObjectCreation) } ) } } override fun visitTypeInsn(opcode: Int, type: String) = adapter.run { + if (opcode == NEW) { + invokeIfInTestingCode( + original = {}, + code = { + push(type.canonicalClassName) + invokeStatic(Injections::beforeNewObjectCreation) + } + ) + } visitTypeInsn(opcode, type) if (opcode == ANEWARRAY) { invokeIfInTestingCode( original = {}, code = { dup() - invokeStatic(Injections::onNewObjectCreation) + invokeStatic(Injections::afterNewObjectCreation) } ) } @@ -1164,7 +1175,7 @@ internal class LincheckClassVisitor( original = {}, code = { dup() - invokeStatic(Injections::onNewObjectCreation) + invokeStatic(Injections::afterNewObjectCreation) } ) } @@ -1176,7 +1187,6 @@ internal class LincheckClassVisitor( private inner class MethodCallTransformer(methodName: String, adapter: GeneratorAdapter) : ManagedStrategyMethodVisitor(methodName, adapter) { override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) = adapter.run { - // TODO: ignore safe calls // TODO: do not ignore if (isCoroutineInternalClass(owner)) { invokeInIgnoredSection { @@ -1200,17 +1210,21 @@ internal class LincheckClassVisitor( } if ( name == "" || + owner.startsWith("sun/nio/ch/lincheck/") || owner.startsWith("org/jetbrains/kotlinx/lincheck/") || owner == "kotlin/jvm/internal/Intrinsics" || owner == "java/util/Objects" || - owner == "java/lang/StringBuilder" || - owner == "java/util/Locale" || owner == "java/lang/String" || - owner == "org/slf4j/helpers/Util" || - owner == "java/util/Properties" || owner == "java/lang/Boolean" || + owner == "java/lang/Long" || owner == "java/lang/Integer" || - owner == "java/lang/Long" + owner == "java/lang/Short" || + owner == "java/lang/Byte" || + owner == "java/lang/Double" || + owner == "java/lang/Float" || + owner == "java/util/Locale" || + owner == "org/slf4j/helpers/Util" || + owner == "java/util/Properties" ) { visitMethodInsn(opcode, owner, name, desc, itf) return @@ -1508,10 +1522,4 @@ private object CoroutineInternalCallTracker { } fun isCoroutineInternalClass(internalClassName: String): Boolean = internalClassName in coroutineInternalClasses -} - -internal enum class TransformationMode { - STRESS, - MODEL_CHECKING, - VERIFICATION } \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt new file mode 100644 index 000000000..bf70f65a1 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -0,0 +1,391 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2024 JetBrains s.r.o. + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed + * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.jetbrains.kotlinx.lincheck.transformation + +import net.bytebuddy.agent.* +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.* +import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.nonTransformedClasses +import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.shouldTransform +import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE +import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentation +import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentationMode +import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentedClassesInTheModelCheckingMode +import org.objectweb.asm.* +import java.io.* +import java.lang.instrument.* +import java.lang.reflect.* +import java.security.* +import java.util.* +import java.util.concurrent.* +import java.util.jar.* + +/** + * Executes [block] with the Lincheck java agent for byte-code instrumentation. + */ +internal inline fun withLincheckJavaAgent(instrumentationMode: InstrumentationMode, block: () -> Unit) { + LincheckJavaAgent.install(instrumentationMode) + return try { + block() + } finally { + LincheckJavaAgent.uninstall() + } +} + +internal enum class InstrumentationMode { + /** + * In this mode, Lincheck transforms bytecode + * only to track coroutine suspensions. + */ + STRESS, + + /** + * In this mode, Lincheck tracks + * all shared memory manipulations. + */ + MODEL_CHECKING +} + +/** + * LincheckJavaAgent represents the Lincheck Java agent responsible for instrumenting bytecode. + * + * @property instrumentation The ByteBuddy instrumentation instance. + * @property instrumentationMode The instrumentation mode to determine which classes to transform. + */ +internal object LincheckJavaAgent { + /** + * The [Instrumentation] instance is used to perform bytecode transformations during runtime. + */ + private val instrumentation = ByteBuddyAgent.install() + + /** + * Determines how to transform classes; + * see [InstrumentationMode.STRESS] and [InstrumentationMode.MODEL_CHECKING]. + */ + lateinit var instrumentationMode: InstrumentationMode + + /** + * Indicates whether the "bootstrap.jar" (see the "bootstrap" project module) + * is added to the bootstrap class loader classpath. + * See [install] for details. + */ + private var isBootstrapJarAddedToClasspath = false + + /** + * TODO + */ + val instrumentedClassesInTheModelCheckingMode = HashSet() + + /** + * Dynamically attaches [LincheckClassFileTransformer] to this JVM instance. + * Please note that the dynamic attach feature will be disabled in future JVM releases, + * but at the moment of implementing this logic (March 2024), it was the smoothest way + * to inject code in the user codebase when the `java.base` module also needs to be instrumented. + */ + fun install(instrumentationMode: InstrumentationMode) { + this.instrumentationMode = instrumentationMode + // The bytecode injections must be loaded with the bootstrap class loader, + // as the `java.base` module is loaded with it. To achieve that, we pack the + // classes related to the bytecode injections in a separate JAR (see the + // "bootstrap" project module), and add it to the bootstrap classpath. + if (!isBootstrapJarAddedToClasspath) { // don't do this twice. + appendBootstrapJarToClassLoaderSearch() + isBootstrapJarAddedToClasspath = true + } + // Add the Lincheck bytecode transformer to this JVM instance, + // allowing already loaded classes re-transformation. + instrumentation.addTransformer(LincheckClassFileTransformer, true) + // The transformation logic depends on the testing strategy. + // In the stress testing mode, Lincheck needs to track coroutine suspensions, + // so it processes all classes (including those that are already loaded), + // and looks for suspension points. In case of the model checking, Lincheck + // could also process all the classes, but it would lead to a significant + // performance degradation. Instead, in the model checking mode, Lincheck + // processes classes lazily, only when they are used. However, we have an + // option to enable the global transformation in the model checking mode + // for testing purposes. + if (instrumentationMode == STRESS || INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { + // Re-transform the already loaded classes. + // New classes will be transformed automatically. + instrumentation.retransformClasses(*getLoadedClassesToInstrument().toTypedArray()) + } + } + + private fun appendBootstrapJarToClassLoaderSearch() { + // The "bootstrap" module is packed to "bootstrap.jar", + // which is in this JAR resources. We need to append this + // "bootstrap.jar" to the bootstrap class loader classpath. + // However, it is impossible to instantiate a `File` instance + // that leads to a file inside a JAR archive. Therefore, + // we copy "bootstrap.jar" to a temporary file, adding this + // temporary file to the bootstrap class loader classpath later on. + val bootstrapJarAsStream = this.javaClass.getResourceAsStream("/bootstrap.jar") + val tempBootstrapJarFile = File.createTempFile("lincheck-bootstrap", ".jar") + bootstrapJarAsStream.use { input -> + tempBootstrapJarFile.outputStream().use { fileOut -> + input!!.copyTo(fileOut) + } + } + instrumentation.appendToBootstrapClassLoaderSearch(JarFile(tempBootstrapJarFile)) + } + + private fun getLoadedClassesToInstrument() = + instrumentation.allLoadedClasses + .filter(instrumentation::isModifiableClass) + .filter { shouldTransform(it.name, instrumentationMode) } + + /** + * Detaches [LincheckClassFileTransformer] from this JVM instance and re-transforms + * the transformed classes to remove the Lincheck injections. + */ + fun uninstall() { + // Remove the Lincheck transformer. + instrumentation.removeTransformer(LincheckClassFileTransformer) + // Collect the original bytecode of the instrumented classes. + val classDefinitions = getLoadedClassesToInstrument() + .filter { + // Filter classes that were transformed by Lincheck and should be restored. + if (instrumentationMode == MODEL_CHECKING || !INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { + it.name in instrumentedClassesInTheModelCheckingMode + } else { + true + } + }.mapNotNull { clazz -> + // For each class, get its original bytecode. + val bytes = nonTransformedClasses[clazz.name] + bytes?.let { ClassDefinition(clazz, it) } + } + // Redefine the instrumented classes back to their original state + // using the original bytecodes collected previously. + instrumentation.redefineClasses(*classDefinitions.toTypedArray()) + // Clear the set of classes instrumented in the model checking mode. + instrumentedClassesInTheModelCheckingMode.clear() + } + + /** + * Ensures that the specified class and all its superclasses are transformed. + * + * @param className The name of the class to be transformed. + */ + fun ensureClassAndAllSuperClassesAreTransformed(className: String) { + if (INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { + Class.forName(className) + return + } + if (className in instrumentedClassesInTheModelCheckingMode) return // already instrumented + ensureClassAndAllSuperClassesAreTransformed(Class.forName(className), Collections.newSetFromMap(IdentityHashMap())) + } + + + /** + * Ensures that the given object and all its referenced objects are transformed for Lincheck analysis. + * If the INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE flag is set to true, no transformation is performed. + * + * @param testInstance the object to be transformed + */ + fun ensureObjectIsTransformed(testInstance: Any) { + if (INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { + return + } + ensureObjectIsTransformedImpl(testInstance, Collections.newSetFromMap(IdentityHashMap())) + } + + /** + * Ensures that the given class and all its superclasses are transformed if necessary. + * + * @param clazz the class to transform + */ + private fun ensureClassAndAllSuperClassesAreTransformed(clazz: Class<*>) { + if (INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { + return + } + if (clazz.name in instrumentedClassesInTheModelCheckingMode) return // already instrumented + ensureClassAndAllSuperClassesAreTransformed(clazz, Collections.newSetFromMap(IdentityHashMap())) + } + + /** + * Ensures that the given object and all its referenced objects are transformed according to the provided rules. + * The transformation is performed recursively, starting from the given object. + * + * @param obj The object to be ensured for transformation. + * @param processedObjects A set of processed objects to avoid infinite recursion. + */ + private fun ensureObjectIsTransformedImpl(obj: Any, processedObjects: MutableSet) { + if (!instrumentation.isModifiableClass(obj.javaClass) || !shouldTransform(obj.javaClass.name, instrumentationMode)) { + return + } + + if (processedObjects.contains(obj)) return + processedObjects += obj + + var clazz: Class<*> = obj.javaClass + + ensureClassAndAllSuperClassesAreTransformed(clazz) + + while (true) { + clazz.declaredFields + .filter { !it.type.isPrimitive } + .filter { !Modifier.isStatic(it.modifiers) } + .mapNotNull { readFieldViaUnsafe(obj, it) } + .forEach { + ensureObjectIsTransformedImpl(it, processedObjects) + } + clazz = clazz.superclass ?: break + } + } + + /** + * Ensures that the given class and all its superclasses are transformed. + * + * @param clazz The class to be transformed. + * @param processedObjects Set of objects that have already been processed to prevent duplicate transformation. + */ + private fun ensureClassAndAllSuperClassesAreTransformed(clazz: Class<*>, processedObjects: MutableSet) { + if (instrumentation.isModifiableClass(clazz) && shouldTransform(clazz.name, instrumentationMode)) { + instrumentedClassesInTheModelCheckingMode += clazz.name + instrumentation.retransformClasses(clazz) + } else { + return + } + // Traverse static fields. + clazz.declaredFields + .filter { !it.type.isPrimitive } + .filter { Modifier.isStatic(it.modifiers) } + .mapNotNull { readFieldViaUnsafe(null, it) } + .forEach { + ensureObjectIsTransformedImpl(it, processedObjects) + } + clazz.superclass?.let { + if (it.name in instrumentedClassesInTheModelCheckingMode) return // already instrumented + ensureClassAndAllSuperClassesAreTransformed(it, processedObjects) + } + } + + private fun readFieldViaUnsafe(obj: Any?, field: Field): Any? = + if (Modifier.isStatic(field.modifiers)) { + val base = UnsafeHolder.UNSAFE.staticFieldBase(field) + val offset = UnsafeHolder.UNSAFE.staticFieldOffset(field) + UnsafeHolder.UNSAFE.getObject(base, offset) + } else { + val offset = UnsafeHolder.UNSAFE.objectFieldOffset(field) + UnsafeHolder.UNSAFE.getObject(obj, offset) + } + + /** + * FOR TEST PURPOSE ONLY! + * To test the byte-code transformation correctness for the + * model-checking strategy, we can transform all classes. + */ + internal val INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE = + System.getProperty("lincheck.instrument_all_classes_in_model_checking_mode")?.toBoolean() ?: false +} + +internal object LincheckClassFileTransformer : ClassFileTransformer { + /* + * In order not to transform the same class several times, + * Lincheck caches the transformed bytes in this object. + * Notice that the transformation depends on the [InstrumentationMode]. + * Additionally, this object caches bytes of non-transformed classes. + */ + private val transformedClassesModelChecking = ConcurrentHashMap() + private val transformedClassesStress = ConcurrentHashMap() + val nonTransformedClasses = ConcurrentHashMap() + + private val transformedClassesCache + get() = when (instrumentationMode) { + STRESS -> transformedClassesStress + MODEL_CHECKING -> transformedClassesModelChecking + } + + override fun transform( + loader: ClassLoader?, className: String, classBeingRedefined: Class<*>?, protectionDomain: ProtectionDomain?, classBytes: ByteArray + ): ByteArray? = runInIgnoredSection { + if (instrumentationMode == MODEL_CHECKING && !INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { + // In the model checking mode, we transform classes lazily, + // once they are used in the testing code. + if (className.canonicalClassName !in instrumentedClassesInTheModelCheckingMode) return null + } else { + if (!shouldTransform(className.canonicalClassName, instrumentationMode)) return null + } + return transformImpl(loader, className, classBytes) + } + + private fun transformImpl(loader: ClassLoader?, className: String, classBytes: ByteArray): ByteArray = transformedClassesCache.computeIfAbsent(className) { + nonTransformedClasses[className] = classBytes + val reader = ClassReader(classBytes) + val writer = SafeClassWriter(reader, loader, ClassWriter.COMPUTE_FRAMES) + try { + reader.accept(LincheckClassVisitor(instrumentationMode, writer), ClassReader.SKIP_FRAMES) + writer.toByteArray() + } catch (e: Throwable) { + System.err.println("Unable to transform $className") + e.printStackTrace() + classBytes + } + } + + @Suppress("SpellCheckingInspection") + fun shouldTransform(className: String, instrumentationMode: InstrumentationMode): Boolean { + // In the stress testing mode, we can simply skip the standard + // Java and Kotlin classes -- they do not have coroutine suspension points. + if (instrumentationMode == STRESS) { + if (className.startsWith("java.") || className.startsWith("kotlin.")) return false + } + // We do not need to instrument most standard Java classes. + // It is fine to inject the Lincheck analysis only into the + // `java.util.*` ones, ignored the known atomic constructs. + if (className.startsWith("java.")) { + if (className.startsWith("java.util.concurrent.") && className.contains("Atomic")) return false + if (className.startsWith("java.util.")) return true + if (className.startsWith("com.sun.")) return false + return false + } + if (className.startsWith("sun.")) return false + if (className.startsWith("javax.")) return false + if (className.startsWith("jdk.")) return false + // We do not need to instrument most standard Kotlin classes. + // However, we need to inject the Lincheck analysis into the classes + // related to collections, iterators, random and coroutines. + if (className.startsWith("kotlin.")) { + if (className.startsWith("kotlin.collections.")) return true + if (className.startsWith("kotlin.jvm.internal.Array") && className.contains("Iterator")) return true + if (className.startsWith("kotlin.ranges.")) return true + if (className.startsWith("kotlin.random.")) return true + if (className.startsWith("kotlin.coroutines.jvm.internal.")) return false + if (className.startsWith("kotlin.coroutines.")) return true + return false + } + if (className.startsWith("kotlinx.atomicfu.")) return false + // We need to skip the classes related to the debugger support in Kotlin coroutines. + if (className.startsWith("kotlinx.coroutines.debug.")) return false + if (className == "kotlinx.coroutines.DebugKt") return false + // We should never transform the coverage-related classes. + if (className.startsWith("com.intellij.rt.coverage.")) return false + // We can also safely do not instrument some libraries for performance reasons. + if (className.startsWith("com.esotericsoftware.kryo.")) return false + if (className.startsWith("net.bytebuddy.")) return false + if (className.startsWith("net.rubygrapefruit.platform.")) return false + if (className.startsWith("io.mockk.")) return false + if (className.startsWith("it.unimi.dsi.fastutil.")) return false + if (className.startsWith("worker.org.gradle.")) return false + if (className.startsWith("org.objectweb.asm.")) return false + if (className.startsWith("org.gradle.")) return false + if (className.startsWith("org.slf4j.")) return false + if (className.startsWith("org.apache.commons.lang.")) return false + if (className.startsWith("org.junit.")) return false + if (className.startsWith("junit.framework.")) return false + // Finally, we should never instrument the Lincheck classes. + if (className.startsWith("org.jetbrains.kotlinx.lincheck.")) return false + if (className.startsWith("sun.nio.ch.lincheck.")) return false + // All the classes that were not filtered out are eligible for transformation. + return true + } +} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java new file mode 100644 index 000000000..d04c5c879 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java @@ -0,0 +1,183 @@ +/*** + * Copyright (c) 2000-2011 INRIA, France Telecom + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jetbrains.kotlinx.lincheck.transformation; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; +import java.io.InputStream; + + +/** + * Copied from https://github.com/rohanpadhye/JQF/blob/master/instrument/src/main/java/janala/instrument/SafeClassWriter.java + * + * A ClassWriter that computes the common super class of two classes without + * actually loading them with a ClassLoader. + */ +public class SafeClassWriter extends ClassWriter { + + private final ClassLoader loader; + + + public SafeClassWriter(ClassReader cr, ClassLoader loader, final int flags) { + super(cr, flags); + this.loader = loader != null ? loader : ClassLoader.getSystemClassLoader(); + } + + @Override + protected String getCommonSuperClass(final String type1, final String type2) { + try { + ClassReader info1 = typeInfo(type1); + ClassReader info2 = typeInfo(type2); + if ((info1.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (typeImplements(type2, info2, type1)) { + return type1; + } else { + return "java/lang/Object"; + } + } + if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (typeImplements(type1, info1, type2)) { + return type2; + } else { + return "java/lang/Object"; + } + } + StringBuilder b1 = typeAncestors(type1, info1); + StringBuilder b2 = typeAncestors(type2, info2); + String result = "java/lang/Object"; + int end1 = b1.length(); + int end2 = b2.length(); + while (true) { + int start1 = b1.lastIndexOf(";", end1 - 1); + int start2 = b2.lastIndexOf(";", end2 - 1); + if (start1 != -1 && start2 != -1 + && end1 - start1 == end2 - start2) { + String p1 = b1.substring(start1 + 1, end1); + String p2 = b2.substring(start2 + 1, end2); + if (p1.equals(p2)) { + result = p1; + end1 = start1; + end2 = start2; + } else { + return result; + } + } else { + return result; + } + } + } catch (IOException e) { + throw new RuntimeException(e.toString()); + } + } + + /** + * Returns the internal names of the ancestor classes of the given type. + * + * @param type + * the internal name of a class or interface. + * @param info + * the ClassReader corresponding to 'type'. + * @return a StringBuilder containing the ancestor classes of 'type', + * separated by ';'. The returned string has the following format: + * ";type1;type2 ... ;typeN", where type1 is 'type', and typeN is a + * direct subclass of Object. If 'type' is Object, the returned + * string is empty. + * @throws IOException + * if the bytecode of 'type' or of some of its ancestor class + * cannot be loaded. + */ + private StringBuilder typeAncestors(String type, ClassReader info) + throws IOException { + StringBuilder b = new StringBuilder(); + while (!"java/lang/Object".equals(type)) { + b.append(';').append(type); + type = info.getSuperName(); + info = typeInfo(type); + } + return b; + } + + /** + * Returns true if the given type implements the given interface. + * + * @param type + * the internal name of a class or interface. + * @param info + * the ClassReader corresponding to 'type'. + * @param itf + * the internal name of a interface. + * @return true if 'type' implements directly or indirectly 'itf' + * @throws IOException + * if the bytecode of 'type' or of some of its ancestor class + * cannot be loaded. + */ + private boolean typeImplements(String type, ClassReader info, String itf) + throws IOException { + while (!"java/lang/Object".equals(type)) { + String[] interfaces = info.getInterfaces(); + for (String string : interfaces) { + if (string.equals(itf)) { + return true; + } + } + for (String s : interfaces) { + if (typeImplements(s, typeInfo(s), itf)) { + return true; + } + } + type = info.getSuperName(); + info = typeInfo(type); + } + return false; + } + + /** + * Returns a ClassReader corresponding to the given class or interface. + * + * @param type + * the internal name of a class or interface. + * @return the ClassReader corresponding to 'type'. + * @throws IOException + * if the bytecode of 'type' cannot be loaded. + */ + private ClassReader typeInfo(final String type) throws IOException { + String resource = type + ".class"; + InputStream is = loader.getResourceAsStream(resource); + try (is) { + if (is == null) { + throw new IOException("Cannot create ClassReader for type " + type); + } + return new ClassReader(is); + } + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt index 83e178966..e4fcf2efa 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt @@ -10,12 +10,13 @@ package org.jetbrains.kotlinx.lincheck.transformation -import org.jetbrains.kotlinx.lincheck.Injections +import sun.nio.ch.lincheck.Injections import org.jetbrains.kotlinx.lincheck.beforeEvent import org.objectweb.asm.* import org.objectweb.asm.Type.* import org.objectweb.asm.commons.* import java.io.* +import java.util.* import java.util.concurrent.* import kotlin.reflect.* import kotlin.reflect.jvm.* @@ -164,6 +165,7 @@ internal inline fun GeneratorAdapter.invokeInIgnoredSection( private val isCoroutineStateMachineClassMap = ConcurrentHashMap() internal fun isCoroutineStateMachineClass(internalClassName: String): Boolean { if (internalClassName.startsWith("java/")) return false + if (internalClassName.startsWith("kotlin/") && !internalClassName.startsWith("kotlin/coroutines/")) return false return isCoroutineStateMachineClassMap.computeIfAbsent(internalClassName) { getSuperclassName(internalClassName) == "kotlin/coroutines/jvm/internal/ContinuationImpl" } @@ -193,46 +195,6 @@ private fun getSuperclassName(internalClassName: String): String? { internal const val ASM_API = Opcodes.ASM9 -// Converts the internal JVM name to a canonical one -internal val String.canonicalClassName get() = this.replace('/', '.') - internal val STRING_TYPE = getType(String::class.java) internal val CLASS_TYPE = getType(Class::class.java) internal val CLASS_FOR_NAME_METHOD = Method("forName", CLASS_TYPE, arrayOf(STRING_TYPE)) - -internal val NOT_TRANSFORMED_JAVA_UTIL_CLASSES = setOf( - "java/util/ServiceLoader", // can not be transformed because of access to `SecurityManager` - "java/util/concurrent/TimeUnit", // many not transformed interfaces such as `java.util.concurrent.BlockingQueue` use it - "java/util/OptionalDouble", // used by `java.util.stream.DoubleStream`. Is an immutable collection - "java/util/OptionalLong", - "java/util/OptionalInt", - "java/util/Optional", - "java/util/Locale", // is an immutable class too - "java/util/Locale\$Category", - "java/util/Locale\$FilteringMode", - "java/util/Currency", - "java/util/Date", - "java/util/Calendar", - "java/util/TimeZone", - "java/util/DoubleSummaryStatistics", // this class is mutable, but `java.util.stream.DoubleStream` interface better be not transformed - "java/util/LongSummaryStatistics", - "java/util/IntSummaryStatistics", - "java/util/Formatter", - "java/util/stream/PipelineHelper", - "java/util/Random", // will be thread safe after `RandomTransformer` transformation - "java/util/concurrent/ThreadLocalRandom" -) -internal val TRANSFORMED_JAVA_UTIL_INTERFACES = setOf( - "java/util/concurrent/CompletionStage", // because it uses `java.util.concurrent.CompletableFuture` - "java/util/Observer", // uses `java.util.Observable` - "java/util/concurrent/RejectedExecutionHandler", - "java/util/concurrent/ForkJoinPool\$ForkJoinWorkerThreadFactory", - "java/util/jar/Pack200\$Packer", - "java/util/jar/Pack200\$Unpacker", - "java/util/prefs/PreferencesFactory", - "java/util/ResourceBundle\$CacheKeyReference", - "java/util/prefs/PreferenceChangeListener", - "java/util/prefs/NodeChangeListener", - "java/util/logging/Filter", - "java/util/spi/ResourceBundleControlProvider" -) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt index 4a29d665c..60fd60746 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt @@ -12,10 +12,9 @@ package org.jetbrains.kotlinx.lincheck.verifier import kotlinx.coroutines.* import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.Injections.lastSuspendedCancellableContinuationDuringVerification -import org.jetbrains.kotlinx.lincheck.transformation.TransformationMode import org.jetbrains.kotlinx.lincheck.verifier.LTS.* import org.jetbrains.kotlinx.lincheck.verifier.OperationType.* +import sun.nio.ch.lincheck.Injections.lastSuspendedCancellableContinuationDuringVerification import java.util.* import kotlin.coroutines.* import kotlin.math.* @@ -43,11 +42,7 @@ typealias ResumedTickets = Set * Practically, Kotlin implementation of such operations via suspend functions is supported. */ -class LTS(sequentialSpecification: Class<*>) { - // we should transform the specification with `CancellabilitySupportClassTransformer` - private val sequentialSpecification: Class<*> = LincheckClassLoader(TransformationMode.VERIFICATION) - .loadClass(sequentialSpecification.name)!! - +class LTS(private val sequentialSpecification: Class<*>) { /** * Cache with all LTS states in order to reuse the equivalent ones. * Equivalency relation among LTS states is defined by the [StateInfo] class. diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/CustomGuaranteesTests.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/CustomGuaranteesTests.kt new file mode 100644 index 000000000..0f846d6a8 --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/CustomGuaranteesTests.kt @@ -0,0 +1,91 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2024 JetBrains s.r.o. + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed + * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.jetbrains.kotlinx.lincheck_test.generator + + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.check +import org.jetbrains.kotlinx.lincheck.strategy.managed.forClasses +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import org.junit.Test +import java.lang.AssertionError + +/** + * Checks that if method has an atomic guarantee, then it won't fail + */ +class CustomAtomicGuaranteeTest { + + private var value = NonAtomicCounter() + + @Operation + fun increment(): Int = value.increment() + + @Test(expected = AssertionError::class) + fun `test without guarantees`() = modelCheckingConfiguration().check(this::class.java) + @Test + fun `test with guarantees`() = modelCheckingConfiguration() + .addGuarantee(forClasses(NonAtomicCounter::class).allMethods().treatAsAtomic()) + .check(this::class.java) + + private fun modelCheckingConfiguration() = ModelCheckingOptions() + .addCustomScenario { + parallel { + thread { actor(CustomAtomicGuaranteeTest::increment) } + thread { actor(CustomAtomicGuaranteeTest::increment) } + } + } + .iterations(0) + + class NonAtomicCounter { + @Volatile + private var value: Int = 0 + fun increment(): Int = value++ + + } +} + + +/** + * Checks that if we mark all methods of the class as atomic/ignored, then no switch points will be added inside, + * even if the method is declared in a superclass + */ +class CustomAtomicGuaranteeWithInheritanceTest { + + private var value = ChildNonAtomicCounter() + @Operation + fun increment(): Int = value.increment() + + @Test(expected = AssertionError::class) + fun `test without guarantees`() = modelCheckingConfiguration() + .check(this::class.java) + @Test + fun `test with guarantees`() = modelCheckingConfiguration() + .addGuarantee(forClasses(ChildNonAtomicCounter::class).allMethods().treatAsAtomic()) + .check(this::class.java) + + open class BaseNonAtomicCounter { + @Volatile + protected var value: Int = 0 + + fun increment(): Int = value++ + } + + class ChildNonAtomicCounter: BaseNonAtomicCounter() + + private fun modelCheckingConfiguration() = ModelCheckingOptions() + .addCustomScenario { + parallel { + thread { actor(CustomAtomicGuaranteeWithInheritanceTest::increment) } + thread { actor(CustomAtomicGuaranteeWithInheritanceTest::increment) } + } + } + .iterations(0) +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/ExpandingRangeGeneratorTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/ExpandingRangeGeneratorTest.kt index d71ef306f..680a50595 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/ExpandingRangeGeneratorTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/generator/ExpandingRangeGeneratorTest.kt @@ -18,7 +18,10 @@ import org.junit.Assert.assertEquals import org.junit.Test import java.util.Random -class ExpandingRangeGeneratorTest { +// This test should be isolated, as the mockK library +// may change some coroutine test output +// (though, have no idea how exactly). +class ExpandingRangeGeneratorIsolatedTest { @Test fun `generator should expand generated values range and than generate values from it`() { diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/FixedActiveThreadsExecutorIsolatedTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/FixedActiveThreadsExecutorIsolatedTest.kt index 5897b5055..6e5eaa02f 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/FixedActiveThreadsExecutorIsolatedTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/FixedActiveThreadsExecutorIsolatedTest.kt @@ -11,7 +11,7 @@ package org.jetbrains.kotlinx.lincheck_test.isolated import org.jetbrains.kotlinx.lincheck.runner.* import org.junit.* -import org.jetbrains.kotlinx.lincheck.TestThread +import sun.nio.ch.lincheck.TestThread import java.util.concurrent.* class FixedActiveThreadsExecutorIsolatedTest { diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/ThreadDumpIsolatedTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/ThreadDumpIsolatedTest.kt index 42997b26d..3f42fa11e 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/ThreadDumpIsolatedTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/isolated/ThreadDumpIsolatedTest.kt @@ -23,8 +23,8 @@ class ThreadDumpIsolatedTest { repeat(30) { val options = StressOptions() .minimizeFailedScenario(false) - .iterations(100_000) - .invocationsPerIteration(1) + .iterations(1) + .invocationsPerIteration(100_000) .invocationTimeout(100) val failure = options.checkImpl(DeadlockOnSynchronizedIsolatedTest::class.java) check(failure is DeadlockOrLivelockFailure) { "${DeadlockOrLivelockFailure::class.simpleName} was expected but ${failure?.javaClass} was obtained"} diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/IllegalModuleAccessOutputMessageTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/IllegalModuleAccessOutputMessageTest.kt deleted file mode 100644 index 986479450..000000000 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/IllegalModuleAccessOutputMessageTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Lincheck - * - * Copyright (C) 2019 - 2023 JetBrains s.r.o. - * - * This Source Code Form is subject to the terms of the - * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed - * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package org.jetbrains.kotlinx.lincheck_test.representation - -import kotlinx.atomicfu.* -import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.annotations.* -import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* -import org.jetbrains.kotlinx.lincheck.util.InternalLincheckExceptionEmulator.throwException -import org.jetbrains.kotlinx.lincheck_test.util.* -import org.junit.* - -/** - * This test checks that hint about classes not accessible from unnamed modules - * is present in output when such exception is occurred. - */ -@Suppress("unused") -class IllegalModuleAccessOutputMessageTest { - - private val counter = atomic(0) - - @Operation - fun incrementTwice() { - counter.incrementAndGet() - counter.decrementAndGet() - } - - @Operation - fun operation() { - if (counter.value != 0) { - throwException { IllegalAccessException("module java.base does not \"opens java.io\" to unnamed module") } - } - } - - @Test - fun test() = ModelCheckingOptions() - .checkImpl(this::class.java) - .checkLincheckOutput("illegal_module_access.txt") -} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ParallelThreadsRunnerExceptionTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ParallelThreadsRunnerExceptionTest.kt index 0a41c7f97..6280aab18 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ParallelThreadsRunnerExceptionTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ParallelThreadsRunnerExceptionTest.kt @@ -16,6 +16,7 @@ import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.runner.UseClocks.* import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.* import org.jetbrains.kotlinx.lincheck_test.verifier.* import org.junit.* import org.junit.Assert.* @@ -152,7 +153,7 @@ class ParallelThreadsRunnerExceptionTest { class ParallelThreadExecutionExceptionsTest { @Test - fun `should fail with unexpected exception results because of classes are not accessible from unnamed modules`() { + fun shouldCompleteWithUnexpectedException() { val scenario = scenario { parallel { thread { actor(::operation) } @@ -162,17 +163,14 @@ class ParallelThreadExecutionExceptionsTest { strategy = mockStrategy(scenario), testClass = this::class.java, validationFunction = null, stateRepresentationFunction = null, timeoutMs = DEFAULT_TIMEOUT_MS, useClocks = RANDOM ).use { runner -> - val results = (runner.run() as UnexpectedExceptionInvocationResult) - val exception = results.exception - - assertTrue(results.exception is RuntimeException) - assertEquals(ADD_OPENS_MESSAGE, exception.message) + val result = runner.run() + check(result is CompletedInvocationResult) } } @Operation fun operation(): Nothing { - throw IllegalAccessException("module java.base does not \"opens java.io\" to unnamed module") + throw IllegalAccessException("unexpected exception") } } diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/SerializableValueTests.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/SerializableValueTests.kt index 7d2d7d09c..e6e42e514 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/SerializableValueTests.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/SerializableValueTests.kt @@ -13,10 +13,7 @@ import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.paramgen.* import org.jetbrains.kotlinx.lincheck.strategy.* -import org.jetbrains.kotlinx.lincheck.transformation.TransformationMode import org.jetbrains.kotlinx.lincheck_test.AbstractLincheckTest -import org.junit.* -import org.junit.Assert.* import java.io.* import java.util.concurrent.atomic.* @@ -68,20 +65,6 @@ class SerializableJavaUtilResultIncorrectTest : AbstractLincheckTest(IncorrectRe } } -class SerializableNullResultTest { - @Test - fun test() { - val a = ValueResult(null) - val value = ValueHolder(0) - val loader = LincheckClassLoader(TransformationMode.STRESS) - val transformedValue = value.convertForLoader(loader) - val b = ValueResult(transformedValue) - // check that no exception was thrown - assertFalse(a == b) - assertFalse(b == a) - } -} - @Param(name = "key", gen = ValueHolderGen::class) class SerializableParameterTest : AbstractLincheckTest() { private val counter = AtomicInteger(0) diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/StaticInitializationDuringAnalysisTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/StaticInitializationDuringAnalysisTest.kt new file mode 100644 index 000000000..6e6168e2f --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/StaticInitializationDuringAnalysisTest.kt @@ -0,0 +1,38 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2024 JetBrains s.r.o. + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed + * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.jetbrains.kotlinx.lincheck_test.transformation + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* +import org.junit.Test +import sun.nio.ch.lincheck.* +import java.util.concurrent.atomic.AtomicInteger + +class StaticInitializationDuringAnalysisTest { + val counter = AtomicInteger() + + @Operation + fun incrementAndGet(): Int { + LoadedDuringAnalysisClass.x + return counter.incrementAndGet() + } + + @Test + fun test() = ModelCheckingOptions().iterations(1).check(this::class) +} + +private object LoadedDuringAnalysisClass { + @JvmStatic + var x = ArrayList().run { + repeat(10000) { add(it) } + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/TransformInterfaceFromJUCWithRemappedClassTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/TransformInterfaceFromJUCWithRemappedClassTest.kt deleted file mode 100644 index 75749b809..000000000 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/TransformInterfaceFromJUCWithRemappedClassTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Lincheck - * - * Copyright (C) 2019 - 2023 JetBrains s.r.o. - * - * This Source Code Form is subject to the terms of the - * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed - * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.jetbrains.kotlinx.lincheck_test.transformation - -import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.annotations.* -import org.jetbrains.kotlinx.lincheck_test.* -import java.util.concurrent.* - -class TransformInterfaceFromJUCWithRemappedClassTest : AbstractLincheckTest() { - private val q: BlockingQueue = ArrayBlockingQueue(10) - - init { - q.add(10) - } - - @Operation - fun op() = q.poll(100, TimeUnit.DAYS) - - override fun > O.customize() { - iterations(1) - actorsBefore(0) - threads(1) - actorsPerThread(1) - actorsAfter(0) - } -} diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/linearizability/RendezvousChannelCustomTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/linearizability/RendezvousChannelCustomTest.kt index 38aaa66df..aaa812b5e 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/linearizability/RendezvousChannelCustomTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/linearizability/RendezvousChannelCustomTest.kt @@ -11,6 +11,8 @@ package org.jetbrains.kotlinx.lincheck_test.verifier.linearizability import kotlinx.coroutines.channels.* import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode +import org.jetbrains.kotlinx.lincheck.transformation.withLincheckJavaAgent import org.jetbrains.kotlinx.lincheck_test.verifier.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* @@ -39,7 +41,7 @@ class RendezvousChannelCustomTest : VerifierState() { private val pollFun = RendezvousChannelCustomTest::poll @Test - fun testCancellation_01() { + fun testCancellation_01() = withLincheckJavaAgent(InstrumentationMode.STRESS) { verify(RendezvousChannelCustomTest::class.java, LinearizabilityVerifier::class.java, { parallel { thread { @@ -53,7 +55,7 @@ class RendezvousChannelCustomTest : VerifierState() { } @Test - fun testCancellation_02() { + fun testCancellation_02() = withLincheckJavaAgent(InstrumentationMode.STRESS) { verify(RendezvousChannelCustomTest::class.java, LinearizabilityVerifier::class.java, { parallel { thread { @@ -67,7 +69,7 @@ class RendezvousChannelCustomTest : VerifierState() { } @Test - fun testCancellation_03() { + fun testCancellation_03() = withLincheckJavaAgent(InstrumentationMode.STRESS) { verify(RendezvousChannelCustomTest::class.java, LinearizabilityVerifier::class.java, { parallel { thread { @@ -81,7 +83,7 @@ class RendezvousChannelCustomTest : VerifierState() { } @Test - fun testCancellation_04() { + fun testCancellation_04() = withLincheckJavaAgent(InstrumentationMode.STRESS){ verify(RendezvousChannelCustomTest::class.java, LinearizabilityVerifier::class.java, { parallel { thread { @@ -95,7 +97,7 @@ class RendezvousChannelCustomTest : VerifierState() { } @Test - fun testCancellation_05() { + fun testCancellation_05() = withLincheckJavaAgent(InstrumentationMode.STRESS) { verify(RendezvousChannelCustomTest::class.java, LinearizabilityVerifier::class.java, { parallel { thread { diff --git a/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt b/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt index f7e16a0c5..a39966e9c 100644 --- a/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt +++ b/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt @@ -13,10 +13,11 @@ The following interleaving leads to the error: | | correct.READ: true at CoroutineCancellationTraceReportingTest.isAbsurd(CoroutineCancellationTraceReportingTest.kt:36) | | | switch | | cancelledOp() + cancel: SUSPENDED + CANCELLED | | +| initCancellability() at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:57) | | | invokeOnCancellation(cancelledOp$2$1) at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:29) | | | getResult(): COROUTINE_SUSPENDED at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:59) | | | trySuspend(): true at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:300) | | -| getParentHandle(): null at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | +| getParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | | CANCELLED BEFORE RESUMPTION | | | _state.get(): InvokeOnCancel#1 at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:0) | | | _state.compareAndSet(InvokeOnCancel#1,CancelledContinuation#1): true at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:209) | | @@ -36,6 +37,12 @@ Detailed trace: | | correct.READ: true at CoroutineCancellationTraceReportingTest.isAbsurd(CoroutineCancellationTraceReportingTest.kt:36) | | | switch | | cancelledOp() + cancel: SUSPENDED + CANCELLED | | +| initCancellability() at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:57) | | +| installParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.initCancellability(CancellableContinuationImpl.kt:129) | | +| _parentHandle.compareAndSet(null,ChildContinuation@1): true at CancellableContinuationImpl.installParentHandle(CancellableContinuationImpl.kt:352) | | +| isCompleted(): false at CancellableContinuationImpl.initCancellability(CancellableContinuationImpl.kt:134) | | +| getState$kotlinx_coroutines_core(): Active@1 at CancellableContinuationImpl.isCompleted(CancellableContinuationImpl.kt:112) | | +| _state.get(): Active@1 at CancellableContinuationImpl.getState$kotlinx_coroutines_core(CancellableContinuationImpl.kt:108) | | | invokeOnCancellation(cancelledOp$2$1) at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:29) | | | invokeOnCancellationImpl(InvokeOnCancel#1) at CancellableContinuationImpl.invokeOnCancellation(CancellableContinuationImpl.kt:399) | | | _state.get(): Active#1 at CancellableContinuationImpl.invokeOnCancellationImpl(CancellableContinuationImpl.kt:403) | | @@ -44,8 +51,8 @@ Detailed trace: | trySuspend(): true at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:300) | | | _decisionAndIndex.get(): 536870911 at CancellableContinuationImpl.trySuspend(CancellableContinuationImpl.kt:0) | | | _decisionAndIndex.compareAndSet(536870911,1073741823): true at CancellableContinuationImpl.trySuspend(CancellableContinuationImpl.kt:278) | | -| getParentHandle(): null at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | -| _parentHandle.get(): null at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | +| getParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | +| _parentHandle.get(): ChildContinuation@1 at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | | CANCELLED BEFORE RESUMPTION | | | _state.get(): InvokeOnCancel#1 at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:0) | | | _state.compareAndSet(InvokeOnCancel#1,CancelledContinuation#1): true at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:209) | | @@ -57,8 +64,9 @@ Detailed trace: | correct.WRITE(false) at CoroutineCancellationTraceReportingTest.setCorrect(CoroutineCancellationTraceReportingTest.kt:23) | | | detachChildIfNonResuable() at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:216) | | | detachChild$kotlinx_coroutines_core() at CancellableContinuationImpl.detachChildIfNonResuable(CancellableContinuationImpl.kt:565) | | -| getParentHandle(): null at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:572) | | -| _parentHandle.get(): null at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | +| getParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:572) | | +| _parentHandle.get(): ChildContinuation@1 at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | +| _parentHandle.set(NonDisposableHandle@1) at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:574) | | | dispatchResume(1) at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:217) | | | tryResume(): false at CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:472) | | | _decisionAndIndex.get(): 1073741823 at CancellableContinuationImpl.tryResume(CancellableContinuationImpl.kt:0) | | diff --git a/src/jvm/test/resources/expected_logs/illegal_module_access.txt b/src/jvm/test/resources/expected_logs/illegal_module_access.txt deleted file mode 100644 index 0c099e6e4..000000000 --- a/src/jvm/test/resources/expected_logs/illegal_module_access.txt +++ /dev/null @@ -1,48 +0,0 @@ -= The execution failed with an unexpected exception = -| ------------------------------ | -| Thread 1 | Thread 2 | -| ------------------------------ | -| incrementTwice() | operation() | -| ------------------------------ | - -java.lang.RuntimeException: It seems that you use Java 9+ and the code uses Unsafe or similar constructions that are not accessible from unnamed modules. -Please add the following lines to your test running configuration: ---add-opens java.base/java.lang=ALL-UNNAMED ---add-opens java.base/jdk.internal.misc=ALL-UNNAMED ---add-exports java.base/jdk.internal.util=ALL-UNNAMED ---add-exports java.base/sun.security.action=ALL-UNNAMED - at org.jetbrains.kotlinx.lincheck.UtilsKt.wrapInvalidAccessFromUnnamedModuleExceptionWithDescription(Utils.kt:338) - at org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy.onFailure(ManagedStrategy.kt:330) - at org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategyRunner.onFailure(ManagedStrategy.kt:1105) - at org.jetbrains.kotlinx.lincheck.runner.TestThreadExecution.failOnExceptionIsUnexpected(TestThreadExecution.java:59) - at org.jetbrains.kotlinx.lincheck.runner.TestThreadExecution226.run(Unknown Source) - at org.jetbrains.kotlinx.lincheck.runner.FixedActiveThreadsExecutor.testThreadRunnable$lambda$8(FixedActiveThreadsExecutor.kt:149) - at java.base/java.lang.Thread.run(Thread.java:833) -Caused by: java.lang.IllegalAccessException: module java.base does not "opens java.io" to unnamed module - at org.jetbrains.kotlinx.lincheck_test.representation.IllegalModuleAccessOutputMessageTest$operation$1.invoke(IllegalModuleAccessOutputMessageTest.kt:39) - at org.jetbrains.kotlinx.lincheck_test.representation.IllegalModuleAccessOutputMessageTest$operation$1.invoke(IllegalModuleAccessOutputMessageTest.kt:39) - at org.jetbrains.kotlinx.lincheck.util.InternalLincheckExceptionEmulator.throwException(InternalLincheckExceptionEmulator.kt:24) - at org.jetbrains.kotlinx.lincheck_test.representation.IllegalModuleAccessOutputMessageTest.operation(IllegalModuleAccessOutputMessageTest.kt:39) - ... 3 more - - -The following interleaving leads to the error: -| ----------------------------------------------------------------------------------------------------------------------------------------------- | -| Thread 1 | Thread 2 | -| ----------------------------------------------------------------------------------------------------------------------------------------------- | -| incrementTwice() | | -| counter.incrementAndGet(): 1 at IllegalModuleAccessOutputMessageTest.incrementTwice(IllegalModuleAccessOutputMessageTest.kt:32) | | -| switch | | -| | operation() | -| ----------------------------------------------------------------------------------------------------------------------------------------------- | - -Detailed trace: -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Thread 1 | Thread 2 | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| incrementTwice() | | -| counter.incrementAndGet(): 1 at IllegalModuleAccessOutputMessageTest.incrementTwice(IllegalModuleAccessOutputMessageTest.kt:32) | | -| switch | | -| | operation() | -| | counter.READ: 1 at IllegalModuleAccessOutputMessageTest.operation(IllegalModuleAccessOutputMessageTest.kt:38) | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt b/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt index 1a930bdc9..3a1700b64 100644 --- a/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt +++ b/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt @@ -6,38 +6,39 @@ | ----------------------------------- | The following interleaving leads to the error: -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Thread 1 | Thread 2 | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| bar(): -1 | | -| barStarted.WRITE(true) at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:38) | | -| lock(null): Unit#1 at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:73) | | -| lock$suspendImpl(MutexImpl#1,null,): Unit#1 at MutexImpl.lock(Mutex.kt:0) | | -| tryLock(null): true at MutexImpl.lock$suspendImpl(Mutex.kt:171) | | -| tryLockImpl(null): 0 at MutexImpl.tryLock(Mutex.kt:183) | | -| tryAcquire(): true at MutexImpl.tryLockImpl(Mutex.kt:189) | | -| owner.get(): at MutexImpl.tryLockImpl(Mutex.kt:190) | | -| switch | | -| | foo(): SUSPENDED + void | -| | barStarted.READ: true at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:29) | -| | canEnterForbiddenBlock.WRITE(true) at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:29) | -| | lock(null): COROUTINE_SUSPENDED at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:63) | -| | lock$suspendImpl(MutexImpl#1,null,): COROUTINE_SUSPENDED at MutexImpl.lock(Mutex.kt:0) | -| | tryLock(null): false at MutexImpl.lock$suspendImpl(Mutex.kt:171) | -| | lockSuspend(null): COROUTINE_SUSPENDED at MutexImpl.lock$suspendImpl(Mutex.kt:172) | -| | acquire() at MutexImpl.lockSuspend(Mutex.kt:177) | -| | getResult(): COROUTINE_SUSPENDED at MutexImpl.lockSuspend(Mutex.kt:321) | -| | trySuspend(): true at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:300) | -| | getParentHandle(): null at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | -| | switch (reason: coroutine is suspended) | -| | result: SUSPENDED + void | -| owner.set(null) at MutexImpl.tryLockImpl(Mutex.kt:191) | | -| counter.READ: 0 at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:40) | | -| counter.WRITE(1) at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:40) | | -| unlock(null) at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:77) | | -| canEnterForbiddenBlock.READ: true at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:42) | | -| result: -1 | | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Thread 1 | Thread 2 | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| bar(): -1 | | +| barStarted.WRITE(true) at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:38) | | +| lock(null): Unit#1 at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:73) | | +| lock$suspendImpl(MutexImpl#1,null,): Unit#1 at MutexImpl.lock(Mutex.kt:0) | | +| tryLock(null): true at MutexImpl.lock$suspendImpl(Mutex.kt:171) | | +| tryLockImpl(null): 0 at MutexImpl.tryLock(Mutex.kt:183) | | +| tryAcquire(): true at MutexImpl.tryLockImpl(Mutex.kt:189) | | +| owner.get(): at MutexImpl.tryLockImpl(Mutex.kt:190) | | +| switch | | +| | foo(): SUSPENDED + void | +| | barStarted.READ: true at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:29) | +| | canEnterForbiddenBlock.WRITE(true) at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:29) | +| | lock(null): COROUTINE_SUSPENDED at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:63) | +| | lock$suspendImpl(MutexImpl#1,null,): COROUTINE_SUSPENDED at MutexImpl.lock(Mutex.kt:0) | +| | tryLock(null): false at MutexImpl.lock$suspendImpl(Mutex.kt:171) | +| | lockSuspend(null): COROUTINE_SUSPENDED at MutexImpl.lock$suspendImpl(Mutex.kt:172) | +| | acquire() at MutexImpl.lockSuspend(Mutex.kt:177) | +| | getResult(): COROUTINE_SUSPENDED at MutexImpl.lockSuspend(Mutex.kt:321) | +| | trySuspend(): true at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:300) | +| | getParentHandle(): null at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | +| | installParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:311) | +| | switch (reason: coroutine is suspended) | +| | result: SUSPENDED + void | +| owner.set(null) at MutexImpl.tryLockImpl(Mutex.kt:191) | | +| counter.READ: 0 at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:40) | | +| counter.WRITE(1) at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:40) | | +| unlock(null) at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:77) | | +| canEnterForbiddenBlock.READ: true at SuspendTraceReportingTest.bar(SuspendTraceReportingTest.kt:42) | | +| result: -1 | | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Detailed trace: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -88,6 +89,8 @@ Detailed trace: | | _decisionAndIndex.compareAndSet(0,536870912): true at CancellableContinuationImpl.trySuspend(CancellableContinuationImpl.kt:278) | | | getParentHandle(): null at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | | _parentHandle.get(): null at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | +| | installParentHandle(): ChildContinuation#1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:311) | +| | _parentHandle.compareAndSet(null,ChildContinuation#1): true at CancellableContinuationImpl.installParentHandle(CancellableContinuationImpl.kt:352) | | | switch (reason: coroutine is suspended) | | | result: SUSPENDED + void | | owner.set(null) at MutexImpl.tryLockImpl(Mutex.kt:191) | | @@ -121,8 +124,9 @@ Detailed trace: | _state.compareAndSet(SemaphoreSegment#1,CompletedContinuation#1): true at CancellableContinuationImpl.tryResumeImpl(CancellableContinuationImpl.kt:541) | | | detachChildIfNonResuable() at CancellableContinuationImpl.tryResumeImpl(CancellableContinuationImpl.kt:542) | | | detachChild$kotlinx_coroutines_core() at CancellableContinuationImpl.detachChildIfNonResuable(CancellableContinuationImpl.kt:565) | | -| getParentHandle(): null at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:572) | | -| _parentHandle.get(): null at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | +| getParentHandle(): ChildContinuation#1 at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:572) | | +| _parentHandle.get(): ChildContinuation#1 at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | +| _parentHandle.set(NonDisposableHandle#1) at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:574) | | | owner.get(): at MutexImpl$CancellableContinuationWithOwner.tryResume(Mutex.kt:264) | | | owner.set(null) at MutexImpl$CancellableContinuationWithOwner.tryResume(Mutex.kt:265) | | | completeResume() at SemaphoreImpl.tryResumeAcquire(Semaphore.kt:349) | | From a55d3a1abe0ce1ad9e5fa6c0c97b66e3fedf5662 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Fri, 5 Apr 2024 16:53:38 +0200 Subject: [PATCH 02/22] Remove useless `runInIgnoredSection`s in FixedActiveThreadsExecutor --- .../org/jetbrains/kotlinx/lincheck/Utils.kt | 2 -- .../runner/FixedActiveThreadsExecutor.kt | 17 ++++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt index fe01c62ce..e01e71231 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt @@ -202,8 +202,6 @@ internal class LincheckInternalBugException(cause: Throwable): Exception(cause) @Suppress("UnusedReceiverParameter") internal inline fun EventTracker.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) @Suppress("UnusedReceiverParameter") -internal inline fun FixedActiveThreadsExecutor.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) -@Suppress("UnusedReceiverParameter") internal inline fun ParallelThreadsRunner.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) @Suppress("UnusedReceiverParameter") internal inline fun LincheckClassFileTransformer.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt index 081758bd4..dcb045558 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt @@ -163,23 +163,18 @@ internal class FixedActiveThreadsExecutor(private val testName: String, private private fun testThreadRunnable(iThread: Int) = Runnable { loop@ while (true) { - // TODO: do we need the added ignored section here? We should be not in the testing code. - val task = runInIgnoredSection { - val task = getTask(iThread) - if (task === Shutdown) return@Runnable - tasks[iThread].value = null // reset task - task as TestThreadExecution - } + val task = getTask(iThread) + if (task === Shutdown) return@Runnable + tasks[iThread].value = null // reset task + task as TestThreadExecution check(task.iThread == iThread) try { task.run() } catch(e: Throwable) { - // TODO: do we need the added ignored section here? We should be not in the testing code. - runInIgnoredSection { setResult(iThread, e) } + setResult(iThread, e) continue@loop } - // TODO: do we need the added ignored section here? We should be not in the testing code. - runInIgnoredSection { setResult(iThread, Done) } + setResult(iThread, Done) } } From 75eb087679a67d7eede1d72c26182423dd7da223 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Fri, 5 Apr 2024 17:23:16 +0200 Subject: [PATCH 03/22] Optimize `runInIgnoredSection` --- .../main/org/jetbrains/kotlinx/lincheck/Utils.kt | 4 +++- .../lincheck/runner/FixedActiveThreadsExecutor.kt | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt index e01e71231..375c7ea26 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt @@ -202,6 +202,8 @@ internal class LincheckInternalBugException(cause: Throwable): Exception(cause) @Suppress("UnusedReceiverParameter") internal inline fun EventTracker.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) @Suppress("UnusedReceiverParameter") +internal inline fun FixedActiveThreadsExecutor.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) +@Suppress("UnusedReceiverParameter") internal inline fun ParallelThreadsRunner.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) @Suppress("UnusedReceiverParameter") internal inline fun LincheckClassFileTransformer.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) @@ -210,7 +212,7 @@ internal inline fun LincheckClassFileTransformer.runInIgnoredSection(block: ( internal inline fun ExecutionClassLoader.runInIgnoredSection(block: () -> R): R = runInIgnoredSection(Thread.currentThread(), block) private inline fun runInIgnoredSection(currentThread: Thread, block: () -> R): R = - if (currentThread is TestThread && !currentThread.inIgnoredSection) { + if (currentThread is TestThread && currentThread.inTestingCode && !currentThread.inIgnoredSection) { currentThread.inIgnoredSection = true try { block() diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt index dcb045558..8c6bd0ea6 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt @@ -163,18 +163,20 @@ internal class FixedActiveThreadsExecutor(private val testName: String, private private fun testThreadRunnable(iThread: Int) = Runnable { loop@ while (true) { - val task = getTask(iThread) - if (task === Shutdown) return@Runnable - tasks[iThread].value = null // reset task - task as TestThreadExecution + val task = runInIgnoredSection { + val task = getTask(iThread) + if (task === Shutdown) return@Runnable + tasks[iThread].value = null // reset task + task as TestThreadExecution + } check(task.iThread == iThread) try { task.run() } catch(e: Throwable) { - setResult(iThread, e) + runInIgnoredSection { setResult(iThread, e) } continue@loop } - setResult(iThread, Done) + runInIgnoredSection { setResult(iThread, Done) } } } From 993f0add1a6977b0889a484701fe0e330a00f0ce Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Fri, 5 Apr 2024 18:24:43 +0200 Subject: [PATCH 04/22] Add "instrumentAllClassesInModelCheckingMode" build property --- build.gradle.kts | 4 ++++ gradle.properties | 1 + .../kotlinx/lincheck/transformation/LincheckJavaAgent.kt | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f42dd6fce..f3108bedb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -110,6 +110,10 @@ tasks { fun Test.configureJvmTestCommon() { maxParallelForks = 1 maxHeapSize = "6g" + val instrumentAllClassesInModelCheckingMode: String by project + if (instrumentAllClassesInModelCheckingMode.toBoolean()) { + systemProperty("lincheck.instrumentAllClassesInModelCheckingMode", "true") + } val extraArgs = mutableListOf() val withEventIdSequentialCheck: String by project if (withEventIdSequentialCheck.toBoolean()) { diff --git a/gradle.properties b/gradle.properties index d1064b786..33cb5865a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,6 +17,7 @@ lastCopyrightYear=2023 jdkToolchainVersion = 17 runAllTestsInSeparateJVMs = false +instrumentAllClassesInModelCheckingMode = false withEventIdSequentialCheck=false kotlinVersion=1.9.21 diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index bf70f65a1..697125963 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -285,7 +285,7 @@ internal object LincheckJavaAgent { * model-checking strategy, we can transform all classes. */ internal val INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE = - System.getProperty("lincheck.instrument_all_classes_in_model_checking_mode")?.toBoolean() ?: false + System.getProperty("lincheck.instrumentAllClassesInModelCheckingMode")?.toBoolean() ?: false } internal object LincheckClassFileTransformer : ClassFileTransformer { From e9a3b1f228b8a0f4621b67a91ba94dd832c63f4c Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Mon, 8 Apr 2024 22:11:59 +0200 Subject: [PATCH 05/22] Update src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt Co-authored-by: Evgeniy Moiseenko Signed-off-by: Nikita Koval --- .../kotlinx/lincheck/transformation/LincheckJavaAgent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index 697125963..c89533a63 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -218,7 +218,7 @@ internal object LincheckJavaAgent { * @param obj The object to be ensured for transformation. * @param processedObjects A set of processed objects to avoid infinite recursion. */ - private fun ensureObjectIsTransformedImpl(obj: Any, processedObjects: MutableSet) { + private fun ensureObjectIsTransformed(obj: Any, processedObjects: MutableSet) { if (!instrumentation.isModifiableClass(obj.javaClass) || !shouldTransform(obj.javaClass.name, instrumentationMode)) { return } From 44a19bbec49aab564055102a07e3cf03209b01cf Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Mon, 8 Apr 2024 22:12:21 +0200 Subject: [PATCH 06/22] Update src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt Co-authored-by: Evgeniy Moiseenko Signed-off-by: Nikita Koval --- .../kotlinx/lincheck/transformation/LincheckJavaAgent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index c89533a63..f7e7e1924 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -203,7 +203,7 @@ internal object LincheckJavaAgent { * * @param clazz the class to transform */ - private fun ensureClassAndAllSuperClassesAreTransformed(clazz: Class<*>) { + private fun ensureClassHierarchyIsTransformed(clazz: Class<*>) { if (INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { return } From 27a910a5dd1a9572ed649e12fcfe8aa9c7e5685a Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Mon, 8 Apr 2024 22:14:48 +0200 Subject: [PATCH 07/22] Move `UnsafeHolder` to the `transformation` package --- .../lincheck/strategy/managed/AtomicFieldUpdaterNames.kt | 2 +- .../kotlinx/lincheck/{ => transformation}/UnsafeHolder.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/jvm/main/org/jetbrains/kotlinx/lincheck/{ => transformation}/UnsafeHolder.kt (92%) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt index aecef04d3..94e2e816d 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt @@ -10,7 +10,7 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed -import org.jetbrains.kotlinx.lincheck.UnsafeHolder.UNSAFE +import org.jetbrains.kotlinx.lincheck.transformation.UnsafeHolder.UNSAFE import java.lang.reflect.Modifier import java.util.concurrent.atomic.AtomicIntegerFieldUpdater import java.util.concurrent.atomic.AtomicLongFieldUpdater diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/UnsafeHolder.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/UnsafeHolder.kt similarity index 92% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/UnsafeHolder.kt rename to src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/UnsafeHolder.kt index 4a78d7c3e..df013997d 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/UnsafeHolder.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/UnsafeHolder.kt @@ -8,7 +8,7 @@ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.jetbrains.kotlinx.lincheck +package org.jetbrains.kotlinx.lincheck.transformation import sun.misc.* From e7580ac32ff82bf17539237271359ad362f7bfc5 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Mon, 8 Apr 2024 22:14:59 +0200 Subject: [PATCH 08/22] Fix compilation issues --- .../strategy/managed/ManagedStrategy.kt | 8 ++++---- .../transformation/LincheckJavaAgent.kt | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt index 680ce00e1..5a8e03d0d 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt @@ -690,13 +690,13 @@ abstract class ManagedStrategy( override fun beforeReadFinalFieldStatic(className: String) = runInIgnoredSection { // We need to ensure all the classes related to the reading object are instrumented. // The following call checks all the static fields. - LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className.canonicalClassName) + LincheckJavaAgent.ensureClassHierarchyIsTransformed(className.canonicalClassName) } override fun beforeReadFieldStatic(className: String, fieldName: String, codeLocation: Int) = runInIgnoredSection { // We need to ensure all the classes related to the reading object are instrumented. // The following call checks all the static fields. - LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className.canonicalClassName) + LincheckJavaAgent.ensureClassHierarchyIsTransformed(className.canonicalClassName) val iThread = currentThread val tracePoint = if (collectTrace) { @@ -843,7 +843,7 @@ abstract class ManagedStrategy( } override fun beforeNewObjectCreation(className: String) = runInIgnoredSection { - LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className) + LincheckJavaAgent.ensureClassHierarchyIsTransformed(className) } override fun afterNewObjectCreation(obj: Any) { @@ -914,7 +914,7 @@ abstract class ManagedStrategy( null -> { if (owner == null) { runInIgnoredSection { - LincheckJavaAgent.ensureClassAndAllSuperClassesAreTransformed(className.canonicalClassName) + LincheckJavaAgent.ensureClassHierarchyIsTransformed(className.canonicalClassName) } } if (collectTrace) { diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index f7e7e1924..9cff7cd1b 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -175,13 +175,13 @@ internal object LincheckJavaAgent { * * @param className The name of the class to be transformed. */ - fun ensureClassAndAllSuperClassesAreTransformed(className: String) { + fun ensureClassHierarchyIsTransformed(className: String) { if (INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { Class.forName(className) return } if (className in instrumentedClassesInTheModelCheckingMode) return // already instrumented - ensureClassAndAllSuperClassesAreTransformed(Class.forName(className), Collections.newSetFromMap(IdentityHashMap())) + ensureClassHierarchyIsTransformed(Class.forName(className), Collections.newSetFromMap(IdentityHashMap())) } @@ -195,7 +195,7 @@ internal object LincheckJavaAgent { if (INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE) { return } - ensureObjectIsTransformedImpl(testInstance, Collections.newSetFromMap(IdentityHashMap())) + ensureObjectIsTransformed(testInstance, Collections.newSetFromMap(IdentityHashMap())) } /** @@ -208,7 +208,7 @@ internal object LincheckJavaAgent { return } if (clazz.name in instrumentedClassesInTheModelCheckingMode) return // already instrumented - ensureClassAndAllSuperClassesAreTransformed(clazz, Collections.newSetFromMap(IdentityHashMap())) + ensureClassHierarchyIsTransformed(clazz, Collections.newSetFromMap(IdentityHashMap())) } /** @@ -228,7 +228,7 @@ internal object LincheckJavaAgent { var clazz: Class<*> = obj.javaClass - ensureClassAndAllSuperClassesAreTransformed(clazz) + ensureClassHierarchyIsTransformed(clazz) while (true) { clazz.declaredFields @@ -236,7 +236,7 @@ internal object LincheckJavaAgent { .filter { !Modifier.isStatic(it.modifiers) } .mapNotNull { readFieldViaUnsafe(obj, it) } .forEach { - ensureObjectIsTransformedImpl(it, processedObjects) + ensureObjectIsTransformed(it, processedObjects) } clazz = clazz.superclass ?: break } @@ -248,7 +248,7 @@ internal object LincheckJavaAgent { * @param clazz The class to be transformed. * @param processedObjects Set of objects that have already been processed to prevent duplicate transformation. */ - private fun ensureClassAndAllSuperClassesAreTransformed(clazz: Class<*>, processedObjects: MutableSet) { + private fun ensureClassHierarchyIsTransformed(clazz: Class<*>, processedObjects: MutableSet) { if (instrumentation.isModifiableClass(clazz) && shouldTransform(clazz.name, instrumentationMode)) { instrumentedClassesInTheModelCheckingMode += clazz.name instrumentation.retransformClasses(clazz) @@ -261,11 +261,11 @@ internal object LincheckJavaAgent { .filter { Modifier.isStatic(it.modifiers) } .mapNotNull { readFieldViaUnsafe(null, it) } .forEach { - ensureObjectIsTransformedImpl(it, processedObjects) + ensureObjectIsTransformed(it, processedObjects) } clazz.superclass?.let { if (it.name in instrumentedClassesInTheModelCheckingMode) return // already instrumented - ensureClassAndAllSuperClassesAreTransformed(it, processedObjects) + ensureClassHierarchyIsTransformed(it, processedObjects) } } From b5a277f2dc4ac7a285b25a8f0411374e6ffe6dfc Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Mon, 8 Apr 2024 22:23:12 +0200 Subject: [PATCH 09/22] Documentation --- .../lincheck/strategy/managed/ManagedStrategy.kt | 2 +- .../lincheck/transformation/LincheckJavaAgent.kt | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt index 5a8e03d0d..fa90288f7 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt @@ -912,7 +912,7 @@ abstract class ManagedStrategy( } null -> { - if (owner == null) { + if (owner == null) { // static method runInIgnoredSection { LincheckJavaAgent.ensureClassHierarchyIsTransformed(className.canonicalClassName) } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index 9cff7cd1b..750e18071 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -173,6 +173,16 @@ internal object LincheckJavaAgent { /** * Ensures that the specified class and all its superclasses are transformed. * + * This function is called before creating a new instance of the specified class + * or reading a static field of it. It ensures that the whole hierarchy of this class + * and the classes of all the static fields (this process is recursive) is transformed. + * Notably, some of these classes may not be loaded yet, and invoking the `` + * during the analysis could cause non-deterministic behaviour (the class initialization + * is invoked only once, while Lincheck relies on the events reproducibility). + * To eliminate the issue, this function also loads the class before transformation, + * thus, initializing it here, in an ignored section of the analysis, re-transforming + * the class after that. + * * @param className The name of the class to be transformed. */ fun ensureClassHierarchyIsTransformed(className: String) { @@ -189,6 +199,8 @@ internal object LincheckJavaAgent { * Ensures that the given object and all its referenced objects are transformed for Lincheck analysis. * If the INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE flag is set to true, no transformation is performed. * + * The function is called upon a test instance creation, to ensure that all the classes related to it are transformed. + * * @param testInstance the object to be transformed */ fun ensureObjectIsTransformed(testInstance: Any) { From 9a0521466568a60d366a8fc6153aceb349fb48c8 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 12 Apr 2024 11:05:21 +0200 Subject: [PATCH 10/22] fix imports Signed-off-by: Evgeniy Moiseenko --- src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt index 1c9a89615..236d2a7d3 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt @@ -21,6 +21,7 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.strategy.managed.getObjectNumber +import org.jetbrains.kotlinx.lincheck.transformation.UnsafeHolder import sun.misc.Unsafe import java.lang.reflect.Field import java.lang.reflect.Modifier From a35eef52f38ff743a4c66cc364654dac58f15886 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 12 Apr 2024 11:06:24 +0200 Subject: [PATCH 11/22] increase test timeout Signed-off-by: Evgeniy Moiseenko --- .../org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt index eac6cab2a..0e703927a 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt @@ -68,4 +68,4 @@ abstract class AbstractLincheckTest( } } -private const val TIMEOUT = 100_000L \ No newline at end of file +private const val TIMEOUT = 2 * 60_000L // 2 min \ No newline at end of file From b47d915f62bc08d34361692ac4d0ada1cb9c3886 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 12 Apr 2024 15:15:05 +0200 Subject: [PATCH 12/22] fix NPE in IncorrectPromptCancellationTest * ensure that the sequential specification class is always instrumented before starting verification Signed-off-by: Evgeniy Moiseenko --- .../org/jetbrains/kotlinx/lincheck/verifier/LTS.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt index 60fd60746..a7b0c98e3 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.verifier.LTS.* import org.jetbrains.kotlinx.lincheck.verifier.OperationType.* +import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent import sun.nio.ch.lincheck.Injections.lastSuspendedCancellableContinuationDuringVerification import java.util.* import kotlin.coroutines.* @@ -264,7 +265,14 @@ class LTS(private val sequentialSpecification: Class<*>) { ).intern(null) { _, _ -> initialState } } - private fun createInitialStateInstance() = sequentialSpecification.newInstance() + private fun createInitialStateInstance(): Any { + return sequentialSpecification.newInstance().also { + // because the sequential version of data structure used for verification + // may differ from the original parallel version, we need to ensure + // that the sequential class is instrumented + LincheckJavaAgent.ensureObjectIsTransformed(it) + } + } private fun StateInfo.computeRemappingFunction(old: StateInfo): RemappingFunction? { if (maxTicket == NO_TICKET) return null From b9e22ca573afc5f273610f22fab3f1ff39b675a9 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 12 Apr 2024 20:08:49 +0200 Subject: [PATCH 13/22] trying to fix SuspendTraceReportingTest * run intercepted continuation code inside ignored section Signed-off-by: Evgeniy Moiseenko --- .../lincheck/runner/ParallelThreadsRunner.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt index fdec47ea2..c36f6f972 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt @@ -140,14 +140,16 @@ internal open class ParallelThreadsRunner( // as it is called in the testing code but should not be analyzed. override fun interceptContinuation(continuation: Continuation): Continuation = runInIgnoredSection { return Continuation(StoreExceptionHandler() + Job()) { result -> - // decrement completed or suspended threads only if the operation was not cancelled - if (!result.cancelledByLincheck()) { - completedOrSuspendedThreads.decrementAndGet() - if (!trySetResumedStatus(iThread, actorId)) { - // already cancelled via prompt cancellation, increment the counter back - completedOrSuspendedThreads.incrementAndGet() + runInIgnoredSection { + // decrement completed or suspended threads only if the operation was not cancelled + if (!result.cancelledByLincheck()) { + completedOrSuspendedThreads.decrementAndGet() + if (!trySetResumedStatus(iThread, actorId)) { + // already cancelled via prompt cancellation, increment the counter back + completedOrSuspendedThreads.incrementAndGet() + } + resWithCont.set(result to continuation as Continuation) } - resWithCont.set(result to continuation as Continuation) } } } From 6b8889966c37c7223a2c9214368b3ec8be89d147 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 12 Apr 2024 20:30:43 +0200 Subject: [PATCH 14/22] fix ResumingFollowUpTest * ensure verifier is called with LincheckJavaAgent installed Signed-off-by: Evgeniy Moiseenko --- .../runner/ResumingFollowUpTest.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt index c0cd83d34..725ce9357 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt @@ -17,6 +17,8 @@ import org.junit.Test import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* import org.jetbrains.kotlinx.lincheck.actor +import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode +import org.jetbrains.kotlinx.lincheck.transformation.withLincheckJavaAgent // Test for early scenario verification completion: follow-up part resumes suspended operation. class ResumingFollowUpTest { @@ -54,16 +56,18 @@ class ResumingFollowUpTest { @Test fun testEarlyScenarioCompletion() { - verify(ResumingFollowUpTest::class.java, LinearizabilityVerifier::class.java, { - parallel { - thread { - operation(actor(f), ValueResult("OK", wasSuspended = true)) + withLincheckJavaAgent(InstrumentationMode.STRESS) { + verify(ResumingFollowUpTest::class.java, LinearizabilityVerifier::class.java, { + parallel { + thread { + operation(actor(f), ValueResult("OK", wasSuspended = true)) + } + thread { + operation(actor(b, 1), ValueResult(true)) + operation(actor(afterB), Suspended) // should be S + 42 + } } - thread { - operation(actor(b, 1), ValueResult(true)) - operation(actor(afterB), Suspended) // should be S + 42 - } - } - }, false) + }, false) + } } } From 69fca7f894737c88696bbfbf267809e8c26cdae4 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 12 Apr 2024 20:51:03 +0200 Subject: [PATCH 15/22] fix all custom scenarios verifier tests * ensure verifier is called with LincheckJavaAgent installed Signed-off-by: Evgeniy Moiseenko --- .../runner/ResumingFollowUpTest.kt | 24 ++++++++----------- .../verifier/CustomScenarioDSL.kt | 11 +++++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt index 725ce9357..c0cd83d34 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/runner/ResumingFollowUpTest.kt @@ -17,8 +17,6 @@ import org.junit.Test import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* import org.jetbrains.kotlinx.lincheck.actor -import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode -import org.jetbrains.kotlinx.lincheck.transformation.withLincheckJavaAgent // Test for early scenario verification completion: follow-up part resumes suspended operation. class ResumingFollowUpTest { @@ -56,18 +54,16 @@ class ResumingFollowUpTest { @Test fun testEarlyScenarioCompletion() { - withLincheckJavaAgent(InstrumentationMode.STRESS) { - verify(ResumingFollowUpTest::class.java, LinearizabilityVerifier::class.java, { - parallel { - thread { - operation(actor(f), ValueResult("OK", wasSuspended = true)) - } - thread { - operation(actor(b, 1), ValueResult(true)) - operation(actor(afterB), Suspended) // should be S + 42 - } + verify(ResumingFollowUpTest::class.java, LinearizabilityVerifier::class.java, { + parallel { + thread { + operation(actor(f), ValueResult("OK", wasSuspended = true)) } - }, false) - } + thread { + operation(actor(b, 1), ValueResult(true)) + operation(actor(afterB), Suspended) // should be S + 42 + } + } + }, false) } } diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/CustomScenarioDSL.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/CustomScenarioDSL.kt index 2f95d83fd..71a6270e8 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/CustomScenarioDSL.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/verifier/CustomScenarioDSL.kt @@ -11,6 +11,7 @@ package org.jetbrains.kotlinx.lincheck_test.verifier import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.transformation.* import org.jetbrains.kotlinx.lincheck.verifier.* /** @@ -42,10 +43,12 @@ fun verify( block: ExecutionBuilder.() -> Unit, correct: Boolean ) { - val (scenario, results) = scenarioWithResults(block) - val verifier = verifierClass.getConstructor(Class::class.java).newInstance(testClass) - val res = verifier.verifyResults(scenario, results) - assert(res == correct) + withLincheckJavaAgent(InstrumentationMode.STRESS) { + val (scenario, results) = scenarioWithResults(block) + val verifier = verifierClass.getConstructor(Class::class.java).newInstance(testClass) + val res = verifier.verifyResults(scenario, results) + assert(res == correct) + } } fun scenarioWithResults( From ccd56a0ebb1ffa8b84f7d77a6e7071b1c5a24a0e Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Mon, 15 Apr 2024 13:55:38 +0200 Subject: [PATCH 16/22] minor fixes after rebase Signed-off-by: Evgeniy Moiseenko --- build.gradle.kts | 13 ------------ ...CoroutineCancellationTraceReportingTest.kt | 2 +- .../expected_logs/coroutine_cancellation.txt | 20 +++++++++---------- .../expected_logs/suspend_trace_reporting.txt | 10 +++++----- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f3108bedb..ad002bc8e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -239,16 +239,3 @@ fun XmlProvider.removeAllLicencesExceptOne(licenceName: String) { } } } - -// We need the Lincheck version in the runtime to check compatibility with the Plugin, -// so we save it into the file in the resources and retrieve in later. -val versionTxt by tasks.registering { - doLast { - val version: String by project - file("src/commonMain/resources/version.txt").writeText(version) - } -} - -tasks.named("processResources") { - dependsOn(versionTxt) -} diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CoroutineCancellationTraceReportingTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CoroutineCancellationTraceReportingTest.kt index 4dd581581..a1500b2cb 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CoroutineCancellationTraceReportingTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CoroutineCancellationTraceReportingTest.kt @@ -44,6 +44,6 @@ class CoroutineCancellationTraceReportingTest : VerifierState() { actorsAfter(0) } .checkImpl(this::class.java) - .checkLincheckOutput( "coroutine_cancellation.txt") + .checkLincheckOutput("coroutine_cancellation.txt") } \ No newline at end of file diff --git a/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt b/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt index a39966e9c..64548c3bf 100644 --- a/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt +++ b/src/jvm/test/resources/expected_logs/coroutine_cancellation.txt @@ -17,7 +17,7 @@ The following interleaving leads to the error: | invokeOnCancellation(cancelledOp$2$1) at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:29) | | | getResult(): COROUTINE_SUSPENDED at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:59) | | | trySuspend(): true at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:300) | | -| getParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | +| getParentHandle(): ChildContinuation#1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | | CANCELLED BEFORE RESUMPTION | | | _state.get(): InvokeOnCancel#1 at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:0) | | | _state.compareAndSet(InvokeOnCancel#1,CancelledContinuation#1): true at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:209) | | @@ -38,11 +38,11 @@ Detailed trace: | | switch | | cancelledOp() + cancel: SUSPENDED + CANCELLED | | | initCancellability() at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:57) | | -| installParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.initCancellability(CancellableContinuationImpl.kt:129) | | -| _parentHandle.compareAndSet(null,ChildContinuation@1): true at CancellableContinuationImpl.installParentHandle(CancellableContinuationImpl.kt:352) | | +| installParentHandle(): ChildContinuation#1 at CancellableContinuationImpl.initCancellability(CancellableContinuationImpl.kt:129) | | +| _parentHandle.compareAndSet(null,ChildContinuation#1): true at CancellableContinuationImpl.installParentHandle(CancellableContinuationImpl.kt:352) | | | isCompleted(): false at CancellableContinuationImpl.initCancellability(CancellableContinuationImpl.kt:134) | | -| getState$kotlinx_coroutines_core(): Active@1 at CancellableContinuationImpl.isCompleted(CancellableContinuationImpl.kt:112) | | -| _state.get(): Active@1 at CancellableContinuationImpl.getState$kotlinx_coroutines_core(CancellableContinuationImpl.kt:108) | | +| getState$kotlinx_coroutines_core(): Active#1 at CancellableContinuationImpl.isCompleted(CancellableContinuationImpl.kt:112) | | +| _state.get(): Active#1 at CancellableContinuationImpl.getState$kotlinx_coroutines_core(CancellableContinuationImpl.kt:108) | | | invokeOnCancellation(cancelledOp$2$1) at CoroutineCancellationTraceReportingTest.cancelledOp(CoroutineCancellationTraceReportingTest.kt:29) | | | invokeOnCancellationImpl(InvokeOnCancel#1) at CancellableContinuationImpl.invokeOnCancellation(CancellableContinuationImpl.kt:399) | | | _state.get(): Active#1 at CancellableContinuationImpl.invokeOnCancellationImpl(CancellableContinuationImpl.kt:403) | | @@ -51,8 +51,8 @@ Detailed trace: | trySuspend(): true at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:300) | | | _decisionAndIndex.get(): 536870911 at CancellableContinuationImpl.trySuspend(CancellableContinuationImpl.kt:0) | | | _decisionAndIndex.compareAndSet(536870911,1073741823): true at CancellableContinuationImpl.trySuspend(CancellableContinuationImpl.kt:278) | | -| getParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | -| _parentHandle.get(): ChildContinuation@1 at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | +| getParentHandle(): ChildContinuation#1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | | +| _parentHandle.get(): ChildContinuation#1 at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | | CANCELLED BEFORE RESUMPTION | | | _state.get(): InvokeOnCancel#1 at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:0) | | | _state.compareAndSet(InvokeOnCancel#1,CancelledContinuation#1): true at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:209) | | @@ -64,9 +64,9 @@ Detailed trace: | correct.WRITE(false) at CoroutineCancellationTraceReportingTest.setCorrect(CoroutineCancellationTraceReportingTest.kt:23) | | | detachChildIfNonResuable() at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:216) | | | detachChild$kotlinx_coroutines_core() at CancellableContinuationImpl.detachChildIfNonResuable(CancellableContinuationImpl.kt:565) | | -| getParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:572) | | -| _parentHandle.get(): ChildContinuation@1 at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | -| _parentHandle.set(NonDisposableHandle@1) at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:574) | | +| getParentHandle(): ChildContinuation#1 at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:572) | | +| _parentHandle.get(): ChildContinuation#1 at CancellableContinuationImpl.getParentHandle(CancellableContinuationImpl.kt:106) | | +| _parentHandle.set(NonDisposableHandle#1) at CancellableContinuationImpl.detachChild$kotlinx_coroutines_core(CancellableContinuationImpl.kt:574) | | | dispatchResume(1) at CancellableContinuationImpl.cancel(CancellableContinuationImpl.kt:217) | | | tryResume(): false at CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:472) | | | _decisionAndIndex.get(): 1073741823 at CancellableContinuationImpl.tryResume(CancellableContinuationImpl.kt:0) | | diff --git a/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt b/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt index 3a1700b64..3244cfdac 100644 --- a/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt +++ b/src/jvm/test/resources/expected_logs/suspend_trace_reporting.txt @@ -21,15 +21,15 @@ The following interleaving leads to the error: | | foo(): SUSPENDED + void | | | barStarted.READ: true at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:29) | | | canEnterForbiddenBlock.WRITE(true) at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:29) | -| | lock(null): COROUTINE_SUSPENDED at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:63) | -| | lock$suspendImpl(MutexImpl#1,null,): COROUTINE_SUSPENDED at MutexImpl.lock(Mutex.kt:0) | +| | lock(null): COROUTINE_SUSPENDED at SuspendTraceReportingTest.foo(SuspendTraceReportingTest.kt:63) | +| | lock$suspendImpl(MutexImpl#1,null,): COROUTINE_SUSPENDED at MutexImpl.lock(Mutex.kt:0) | | | tryLock(null): false at MutexImpl.lock$suspendImpl(Mutex.kt:171) | -| | lockSuspend(null): COROUTINE_SUSPENDED at MutexImpl.lock$suspendImpl(Mutex.kt:172) | +| | lockSuspend(null): COROUTINE_SUSPENDED at MutexImpl.lock$suspendImpl(Mutex.kt:172) | | | acquire() at MutexImpl.lockSuspend(Mutex.kt:177) | -| | getResult(): COROUTINE_SUSPENDED at MutexImpl.lockSuspend(Mutex.kt:321) | +| | getResult(): COROUTINE_SUSPENDED at MutexImpl.lockSuspend(Mutex.kt:321) | | | trySuspend(): true at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:300) | | | getParentHandle(): null at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:310) | -| | installParentHandle(): ChildContinuation@1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:311) | +| | installParentHandle(): ChildContinuation#1 at CancellableContinuationImpl.getResult(CancellableContinuationImpl.kt:311) | | | switch (reason: coroutine is suspended) | | | result: SUSPENDED + void | | owner.set(null) at MutexImpl.tryLockImpl(Mutex.kt:191) | | From a21ee44838bd7b408bacb25a18a0a59a738d5597 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Mon, 15 Apr 2024 17:00:15 +0200 Subject: [PATCH 17/22] move `beforeEvent` hook into `Injections` class under `bootstrap` package Signed-off-by: Evgeniy Moiseenko --- bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt | 1 + bootstrap/src/sun/nio/ch/lincheck/Injections.java | 4 ++++ .../kotlinx/lincheck/strategy/managed/ManagedStrategy.kt | 7 ++++++- .../kotlinx/lincheck/transformation/TransformationUtils.kt | 3 +-- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt index efe341d67..7f3d26717 100644 --- a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt +++ b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt @@ -56,6 +56,7 @@ interface EventTracker { // Methods required for the plugin integration fun shouldInvokeBeforeEvent(): Boolean + fun beforeEvent(eventId: Int, type: String) fun getEventId(): Int fun setLastMethodCallEventId() } \ No newline at end of file diff --git a/bootstrap/src/sun/nio/ch/lincheck/Injections.java b/bootstrap/src/sun/nio/ch/lincheck/Injections.java index 7dc903fcb..c268955bb 100644 --- a/bootstrap/src/sun/nio/ch/lincheck/Injections.java +++ b/bootstrap/src/sun/nio/ch/lincheck/Injections.java @@ -356,6 +356,10 @@ public static boolean shouldInvokeBeforeEvent() { return getEventTracker().shouldInvokeBeforeEvent(); } + public static void beforeEvent(int eventId, String type) { + getEventTracker().beforeEvent(eventId, type); + } + /** * @param type type of the next event. Used only for debug purposes. */ diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt index fa90288f7..db3e7dd2e 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt @@ -9,8 +9,8 @@ */ package org.jetbrains.kotlinx.lincheck.strategy.managed -import kotlinx.coroutines.* import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.beforeEvent as ideaPluginBeforeEvent import org.jetbrains.kotlinx.lincheck.CancellationResult.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.runner.* @@ -24,6 +24,7 @@ import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicFieldUpdaterNames.g import sun.nio.ch.lincheck.* import java.lang.invoke.* import sun.misc.Unsafe +import kotlinx.coroutines.* import java.util.concurrent.atomic.* import java.lang.reflect.* import java.util.* @@ -1130,6 +1131,10 @@ abstract class ManagedStrategy( return constructor(iThread, actorId, callStackTrace.getOrNull(iThread)?.toList() ?: emptyList()) } + override fun beforeEvent(eventId: Int, type: String) { + ideaPluginBeforeEvent(eventId, type) + } + /** * This method is called before [beforeEvent] method call to provide current event (trace point) id. */ diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt index e4fcf2efa..a2126bb73 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt @@ -11,7 +11,6 @@ package org.jetbrains.kotlinx.lincheck.transformation import sun.nio.ch.lincheck.Injections -import org.jetbrains.kotlinx.lincheck.beforeEvent import org.objectweb.asm.* import org.objectweb.asm.Type.* import org.objectweb.asm.commons.* @@ -85,7 +84,7 @@ internal fun GeneratorAdapter.invokeBeforeEvent(debugMessage: String, setMethodE push(debugMessage) invokeStatic(Injections::getNextEventId) push(debugMessage) - invokeStatic(::beforeEvent) + invokeStatic(Injections::beforeEvent) }, elseClause = {} ) From 2c2adfa962cab49e173cf2c7735a20109db08669 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Mon, 15 Apr 2024 17:48:18 +0200 Subject: [PATCH 18/22] reset objects numeration before printing trace Signed-off-by: Evgeniy Moiseenko --- .../lincheck/strategy/managed/TraceReporter.kt | 6 +++++- .../expected_logs/captured_value_plugin.txt | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt index 958ce7b7f..aa9dc2680 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt @@ -25,13 +25,17 @@ internal fun StringBuilder.appendTrace( trace: Trace, exceptionStackTraces: Map ) { + // reset objects numeration + cleanObjectNumeration() + val startTraceGraphNode = constructTraceGraph(failure, results, trace, exceptionStackTraces) appendShortTrace(startTraceGraphNode, failure) appendExceptionsStackTracesBlock(exceptionStackTraces) appendDetailedTrace(startTraceGraphNode, failure) - objectNumeration.clear() // clear the numeration at the end to avoid memory leaks + // clear the numeration at the end to avoid memory leaks + cleanObjectNumeration() } /** diff --git a/src/jvm/test/resources/expected_logs/captured_value_plugin.txt b/src/jvm/test/resources/expected_logs/captured_value_plugin.txt index ea3b01755..1d45d2b38 100644 --- a/src/jvm/test/resources/expected_logs/captured_value_plugin.txt +++ b/src/jvm/test/resources/expected_logs/captured_value_plugin.txt @@ -15,8 +15,8 @@ The following interleaving leads to the error: | | innerClass.READ: InnerClass#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:39) | | | innerClass.READ: InnerClass#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:40) | | | otherInnerClass.READ: InnerClass#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:41) | -| | primitiveArray.READ: int[]#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:42) | -| | objectArray.READ: String[]#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:43) | +| | primitiveArray.READ: int[]#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:42) | +| | objectArray.READ: String[]#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:43) | | | counter.READ: 0 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:44) | | | switch | | operation(): 0 | | @@ -34,8 +34,8 @@ Detailed trace: | | innerClass.READ: InnerClass#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:39) | | | innerClass.READ: InnerClass#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:40) | | | otherInnerClass.READ: InnerClass#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:41) | -| | primitiveArray.READ: int[]#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:42) | -| | objectArray.READ: String[]#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:43) | +| | primitiveArray.READ: int[]#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:42) | +| | objectArray.READ: String[]#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:43) | | | counter.READ: 0 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:44) | | | switch | | operation(): 0 | | @@ -44,8 +44,8 @@ Detailed trace: | innerClass.READ: InnerClass#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:39) | | | innerClass.READ: InnerClass#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:40) | | | otherInnerClass.READ: InnerClass#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:41) | | -| primitiveArray.READ: int[]#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:42) | | -| objectArray.READ: String[]#2 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:43) | | +| primitiveArray.READ: int[]#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:42) | | +| objectArray.READ: String[]#1 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:43) | | | counter.READ: 0 at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:44) | | | counter.WRITE(1) at CapturedValueRepresentationTest.operation(CapturedValueRepresentationTest.kt:44) | | | result: 0 | | From 12ab98ea0f8a3162a04c9a7a64fc9d6eea909f24 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Tue, 16 Apr 2024 12:39:00 +0200 Subject: [PATCH 19/22] minor Signed-off-by: Evgeniy Moiseenko --- gradle.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 33cb5865a..d7409df68 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,9 +15,9 @@ version=2.30-SNAPSHOT inceptionYear=2019 lastCopyrightYear=2023 -jdkToolchainVersion = 17 -runAllTestsInSeparateJVMs = false -instrumentAllClassesInModelCheckingMode = false +jdkToolchainVersion=17 +runAllTestsInSeparateJVMs=false +instrumentAllClassesInModelCheckingMode=false withEventIdSequentialCheck=false kotlinVersion=1.9.21 From e08e4cb2de300d72f3bf91991e47da7dc5770593 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Thu, 18 Apr 2024 19:33:59 +0200 Subject: [PATCH 20/22] moved UnsafeHolder to util package Signed-off-by: Evgeniy Moiseenko --- src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt | 2 +- .../lincheck/strategy/managed/AtomicFieldUpdaterNames.kt | 2 +- .../kotlinx/lincheck/transformation/LincheckJavaAgent.kt | 3 ++- .../kotlinx/lincheck/{transformation => util}/UnsafeHolder.kt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) rename src/jvm/main/org/jetbrains/kotlinx/lincheck/{transformation => util}/UnsafeHolder.kt (92%) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt index 236d2a7d3..3cbe14929 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt @@ -21,7 +21,7 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.strategy.managed.getObjectNumber -import org.jetbrains.kotlinx.lincheck.transformation.UnsafeHolder +import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder import sun.misc.Unsafe import java.lang.reflect.Field import java.lang.reflect.Modifier diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt index 94e2e816d..d0162cec9 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt @@ -10,7 +10,7 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed -import org.jetbrains.kotlinx.lincheck.transformation.UnsafeHolder.UNSAFE +import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder.UNSAFE import java.lang.reflect.Modifier import java.util.concurrent.atomic.AtomicIntegerFieldUpdater import java.util.concurrent.atomic.AtomicLongFieldUpdater diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index 750e18071..424e7bc0c 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -15,10 +15,11 @@ import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.* import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.nonTransformedClasses import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.shouldTransform -import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentation import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentationMode import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentedClassesInTheModelCheckingMode +import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE +import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder import org.objectweb.asm.* import java.io.* import java.lang.instrument.* diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/UnsafeHolder.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt similarity index 92% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/UnsafeHolder.kt rename to src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt index df013997d..cb9495a36 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/UnsafeHolder.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt @@ -8,7 +8,7 @@ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.jetbrains.kotlinx.lincheck.transformation +package org.jetbrains.kotlinx.lincheck.util import sun.misc.* From c727dbfee5cd45d3e743acb3953aef2b2f986115 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Thu, 18 Apr 2024 19:40:03 +0200 Subject: [PATCH 21/22] remove duplicate readFieldViaUnsafe function implementation Signed-off-by: Evgeniy Moiseenko --- .../kotlinx/lincheck/ObjectTraverser.kt | 32 ++++++------------- .../transformation/LincheckJavaAgent.kt | 17 +++------- .../kotlinx/lincheck/util/UnsafeHolder.kt | 17 ++++++++-- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt index 3cbe14929..f0aeeb2c9 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt @@ -21,7 +21,7 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.strategy.managed.getObjectNumber -import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder +import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe import sun.misc.Unsafe import java.lang.reflect.Field import java.lang.reflect.Modifier @@ -119,30 +119,18 @@ private fun readField(obj: Any?, field: Field): Any? { return readFieldViaUnsafe(obj, field, Unsafe::getObject) } return when (field.type) { - Boolean::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getBoolean) - Byte::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getByte) - Char::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getChar) - Short::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getShort) - Int::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getInt) - Long::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getLong) - Double::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getDouble) - Float::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getFloat) - else -> error("No more types expected") + Boolean::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getBoolean) + Byte::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getByte) + Char::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getChar) + Short::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getShort) + Int::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getInt) + Long::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getLong) + Double::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getDouble) + Float::class.javaPrimitiveType -> readFieldViaUnsafe(obj, field, Unsafe::getFloat) + else -> error("No more types expected") } } - -private inline fun readFieldViaUnsafe(obj: Any?, field: Field, extractMethod: Unsafe.(Any?, Long) -> T): T = - if (Modifier.isStatic(field.modifiers)) { - val base = UnsafeHolder.UNSAFE.staticFieldBase(field) - val offset = UnsafeHolder.UNSAFE.staticFieldOffset(field) - UnsafeHolder.UNSAFE.extractMethod(base, offset) - } else { - val offset = UnsafeHolder.UNSAFE.objectFieldOffset(field) - UnsafeHolder.UNSAFE.extractMethod(obj, offset) - } - - private fun isAtomic(value: Any?): Boolean { if (value == null) return false return value.javaClass.canonicalName.let { diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index 424e7bc0c..c33266a7c 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -19,7 +19,8 @@ import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumen import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentationMode import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentedClassesInTheModelCheckingMode import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.INSTRUMENT_ALL_CLASSES_IN_MODEL_CHECKING_MODE -import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder +import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe +import sun.misc.Unsafe import org.objectweb.asm.* import java.io.* import java.lang.instrument.* @@ -247,7 +248,7 @@ internal object LincheckJavaAgent { clazz.declaredFields .filter { !it.type.isPrimitive } .filter { !Modifier.isStatic(it.modifiers) } - .mapNotNull { readFieldViaUnsafe(obj, it) } + .mapNotNull { readFieldViaUnsafe(obj, it, Unsafe::getObject) } .forEach { ensureObjectIsTransformed(it, processedObjects) } @@ -272,7 +273,7 @@ internal object LincheckJavaAgent { clazz.declaredFields .filter { !it.type.isPrimitive } .filter { Modifier.isStatic(it.modifiers) } - .mapNotNull { readFieldViaUnsafe(null, it) } + .mapNotNull { readFieldViaUnsafe(null, it, Unsafe::getObject) } .forEach { ensureObjectIsTransformed(it, processedObjects) } @@ -282,16 +283,6 @@ internal object LincheckJavaAgent { } } - private fun readFieldViaUnsafe(obj: Any?, field: Field): Any? = - if (Modifier.isStatic(field.modifiers)) { - val base = UnsafeHolder.UNSAFE.staticFieldBase(field) - val offset = UnsafeHolder.UNSAFE.staticFieldOffset(field) - UnsafeHolder.UNSAFE.getObject(base, offset) - } else { - val offset = UnsafeHolder.UNSAFE.objectFieldOffset(field) - UnsafeHolder.UNSAFE.getObject(obj, offset) - } - /** * FOR TEST PURPOSE ONLY! * To test the byte-code transformation correctness for the diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt index cb9495a36..d9c52dc96 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt @@ -10,9 +10,11 @@ package org.jetbrains.kotlinx.lincheck.util -import sun.misc.* +import sun.misc.Unsafe +import java.lang.reflect.Field +import java.lang.reflect.Modifier -object UnsafeHolder { +internal object UnsafeHolder { val UNSAFE: Unsafe = try { val unsafeField = Unsafe::class.java.getDeclaredField("theUnsafe") unsafeField.isAccessible = true @@ -20,4 +22,15 @@ object UnsafeHolder { } catch (ex: Exception) { throw RuntimeException("Can't get the Unsafe instance, please report it to the Lincheck team", ex) } +} + +internal inline fun readFieldViaUnsafe(obj: Any?, field: Field, getter: Unsafe.(Any?, Long) -> T): T { + if (Modifier.isStatic(field.modifiers)) { + val base = UnsafeHolder.UNSAFE.staticFieldBase(field) + val offset = UnsafeHolder.UNSAFE.staticFieldOffset(field) + return UnsafeHolder.UNSAFE.getter(base, offset) + } else { + val offset = UnsafeHolder.UNSAFE.objectFieldOffset(field) + return UnsafeHolder.UNSAFE.getter(obj, offset) + } } \ No newline at end of file From 57d97adca91f1f0b293beaa67cfb03371f47df00 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Thu, 18 Apr 2024 19:43:35 +0200 Subject: [PATCH 22/22] fix grammar Signed-off-by: Evgeniy Moiseenko --- src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt index a7b0c98e3..e93bc3cbb 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt @@ -267,9 +267,9 @@ class LTS(private val sequentialSpecification: Class<*>) { private fun createInitialStateInstance(): Any { return sequentialSpecification.newInstance().also { - // because the sequential version of data structure used for verification - // may differ from the original parallel version, we need to ensure - // that the sequential class is instrumented + // the sequential version of the data structure used for verification + // may differ from the original parallel version, + // in this case we need to ensure that the sequential class is also instrumented LincheckJavaAgent.ensureObjectIsTransformed(it) } }