diff --git a/README.md b/README.md
index 4386da9e4..931dacd2a 100755
--- a/README.md
+++ b/README.md
@@ -18,27 +18,33 @@ This is a fork of [Lin-Check framework by Devexperts](https://github.com/Devexpe
Table of contents
=================
- * [Test structure](#test-structure)
- * [Initial state](#initial-state)
- * [Operations and groups](#operations-and-groups)
- * [Calling at most once](#calling-at-most-once)
- * [Exception as a result](#exception-as-a-result)
- * [Operation groups](#operation-groups)
- * [Parameter generators](#parameter-generators)
- * [Binding parameter and generator names](#binding-parameter-and-generator-names)
- * [Sequential specification](#sequential-specification)
- * [Run test](#run-test)
- * [Execution strategies](#execution-strategies)
- * [Stress strategy](#stress-strategy)
- * [Correctness contracts](#correctness-contracts)
- * [Linearizability](#linearizability)
- * [Serializability](#serializability)
- * [Quiescent consistency](#quiescent-consistency)
- * [Blocking data structures](#blocking-data-structures)
- * [Configuration via options](#configuration-via-options)
- * [Sample](#sample)
- * [Contacts](#contacts)
-
+- [Test structure](#test-structure)
+ * [Initial state](#initial-state)
+ * [Operations and groups](#operations-and-groups)
+ + [Calling at most once](#calling-at-most-once)
+ + [Exception as a result](#exception-as-a-result)
+ + [Operation groups](#operation-groups)
+ * [Parameter generators](#parameter-generators)
+ + [Binding parameter and generator names](#binding-parameter-and-generator-names)
+ * [Sequential specification](#sequential-specification)
+ * [Validation functions](#validation-functions)
+ * [Parameter and result types](#parameter-and-result-types)
+ * [Run test](#run-test)
+- [Execution strategies](#execution-strategies)
+ * [Stress strategy](#stress-strategy)
+- [Correctness contracts](#correctness-contracts)
+ * [Linearizability](#linearizability)
+ + [States equivalency](#states-equivalency)
+ + [Test example](#test-example)
+ * [Serializability](#serializability)
+ * [Quiescent consistency](#quiescent-consistency)
+ + [Test example](#test-example-1)
+- [Blocking data structures](#blocking-data-structures)
+ + [Example with a rendezvous channel](#example-with-a-rendezvous-channel)
+ + [States equivalency](#states-equivalency-1)
+ + [Test example](#test-example-2)
+- [Configuration via options](#configuration-via-options)
+- [Example](#example)
@@ -164,6 +170,16 @@ class MyLockFreeListTest {
...
}
```
+
+## Parameter and result types
+The standard parameter generators are provided for the basic types like `Int`, `Float`, or `String`.
+However, it is also possible to implement a custom generator for any parameter type.
+Nevertheless, not all types are supported since **lincheck** performs the byte-code transformation,
+and the same by name classes can differ during the scenario generation phase and the running or verification one.
+However, it is still possible to use non-trivial custom parameters if the corresponding types implement
+`Serializable` interface; this way, **lincheck** transfers the generated parameter between different class loaders
+using the serialization-deserialization mechanism.
+The same problem occurs with non-trivial result types, which should also implement the `Serializable` interface.
## Run test
In order to run a test, `LinChecker.check(...)` method should be executed with the provided test class as a parameter. Then **lincheck** looks at execution strategies to be used, which can be provided using annotations or options (see [Configuration via options](#configuration-via-options) for details), and runs a test with each of provided strategies. If an error is found, an `AssertionError` is thrown and the detailed error information is printed to the standard output. It is recommended to use **JUnit** or similar testing library to run `LinChecker.check(...)` method.
diff --git a/src/main/java/org/jetbrains/kotlinx/lincheck/Result.kt b/src/main/java/org/jetbrains/kotlinx/lincheck/Result.kt
index ec2811933..0d0a856ff 100644
--- a/src/main/java/org/jetbrains/kotlinx/lincheck/Result.kt
+++ b/src/main/java/org/jetbrains/kotlinx/lincheck/Result.kt
@@ -1,5 +1,6 @@
package org.jetbrains.kotlinx.lincheck
+import java.io.*
import kotlin.coroutines.*
/*
@@ -27,7 +28,6 @@ import kotlin.coroutines.*
/**
* The instance of this class represents a result of actor invocation.
*
-
*
If the actor invocation suspended the thread and did not get the final result yet
* though it can be resumed later, then the {@link Type#NO_RESULT no_result result type} is used.
*
@@ -44,8 +44,34 @@ sealed class Result {
/**
* Type of result used if the actor invocation returns any value.
*/
-data class ValueResult @JvmOverloads constructor(val value: Any?, override val wasSuspended: Boolean = false) : Result() {
+class ValueResult @JvmOverloads constructor(val value: Any?, override val wasSuspended: Boolean = false) : Result() {
+ private val valueClassTransformed: Boolean get() = value?.javaClass?.classLoader is TransformationClassLoader
+ private val serializedObject: ByteArray by lazy(LazyThreadSafetyMode.NONE) {
+ check(valueClassTransformed) { "The result value class should be loaded not by the system class loader and be transformed" }
+ check(value is Serializable) {
+ "The result should either be a type always loaded by the system class loader " +
+ "(e.g., Int, String, List) or implement Serializable interface; " +
+ "the actual class is ${value?.javaClass}."
+ }
+ value.serialize()
+ }
+
override fun toString() = wasSuspendedPrefix + "$value"
+
+ override fun equals(other: Any?): Boolean {
+ // Check that the classes are equal by names
+ // since they can be loaded via different class loaders.
+ if (javaClass.name != other?.javaClass?.name) return false
+ other as ValueResult
+ // Is `wasSuspended` flag the same?
+ if (wasSuspended != other.wasSuspended) return false
+ // Do the values coincide ignoring the difference in class loaders?
+ if (valueClassTransformed != other.valueClassTransformed) return false
+ return if (!valueClassTransformed) value == other.value
+ else serializedObject.contentEquals(other.serializedObject)
+ }
+
+ override fun hashCode(): Int = if (wasSuspended) 0 else 1 // we cannot use the value here
}
/**
diff --git a/src/main/java/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java b/src/main/java/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java
index 35d11cf53..b87ce2561 100644
--- a/src/main/java/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java
+++ b/src/main/java/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java
@@ -23,6 +23,7 @@
*/
import org.jetbrains.kotlinx.lincheck.runner.Runner;
+import org.jetbrains.kotlinx.lincheck.strategy.ManagedStrategyHolder;
import org.jetbrains.kotlinx.lincheck.strategy.Strategy;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
@@ -65,11 +66,11 @@ public TransformationClassLoader(Function classTrans
* @param className checking class name
* @return result of checking class
*/
- private static boolean doNotTransform(String className) {
+ static boolean doNotTransform(String className) {
return className == null ||
(className.startsWith("org.jetbrains.kotlinx.lincheck.") &&
!className.startsWith("org.jetbrains.kotlinx.lincheck.test.") &&
- !className.endsWith("ManagedStrategyHolder")) ||
+ !className.equals(ManagedStrategyHolder.class.getName())) ||
className.startsWith("sun.") ||
className.startsWith("java.") ||
className.startsWith("jdk.internal.") ||
diff --git a/src/main/java/org/jetbrains/kotlinx/lincheck/Utils.kt b/src/main/java/org/jetbrains/kotlinx/lincheck/Utils.kt
index 226c67ce9..b93b27736 100644
--- a/src/main/java/org/jetbrains/kotlinx/lincheck/Utils.kt
+++ b/src/main/java/org/jetbrains/kotlinx/lincheck/Utils.kt
@@ -26,13 +26,14 @@ import org.jetbrains.kotlinx.lincheck.CancellableContinuationHolder.storedLastCa
import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.verifier.*
-import java.lang.ref.WeakReference
+import java.io.*
+import java.lang.ClassLoader.*
+import java.lang.ref.*
import java.lang.reflect.*
import java.util.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
-
@Volatile
private var consumedCPU = System.currentTimeMillis().toInt()
@@ -78,7 +79,8 @@ internal fun executeActor(
): Result {
try {
val m = getMethod(instance, actor.method)
- val args = if (actor.isSuspendable) actor.arguments + completion else actor.arguments
+ 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) {
@@ -99,7 +101,7 @@ internal fun executeActor(
}
internal inline fun executeValidationFunctions(instance: Any, validationFunctions: List,
- onError: (functionName: String, exception: Throwable) -> Unit) {
+ onError: (functionName: String, exception: Throwable) -> Unit) {
for (f in validationFunctions) {
val validationException = executeValidationFunction(instance, f)
if (validationException != null) {
@@ -126,12 +128,20 @@ private val methodsCache = WeakHashMap, WeakHashMap.getMethod(name: String, parameterTypes: Array>): Method =
+ methods.find { method ->
+ method.name == name && method.parameterTypes.map { it.name } == parameterTypes.map { it.name }
+ } ?: throw NoSuchMethodException("${getName()}.$name(${parameterTypes.joinToString(",")})")
+
/**
* Creates [Result] of corresponding type from any given value.
*
@@ -163,7 +173,6 @@ private fun kotlin.Result.toLinCheckResult(wasSuspended: Boolean) =
}
} else ExceptionResult.create(exceptionOrNull()!!.let { it::class.java }, wasSuspended)
-
inline fun Throwable.catch(vararg exceptions: Class<*>, block: () -> R): R {
if (exceptions.any { this::class.java.isAssignableFrom(it) }) {
return block()
@@ -210,4 +219,39 @@ fun storeCancellableContinuation(cont: CancellableContinuation<*>) {
} else {
storedLastCancellableCont = cont
}
+}
+
+internal fun ExecutionScenario.convertForLoader(loader: ClassLoader) = ExecutionScenario(
+ initExecution,
+ parallelExecution.map { actors ->
+ actors.map { a ->
+ val args = a.arguments.map { it.convertForLoader(loader) }
+ Actor(a.method, args, a.handledExceptions, a.cancelOnSuspension, a.allowExtraSuspension)
+ }
+ },
+ postExecution
+)
+
+private fun Any?.convertForLoader(loader: ClassLoader) = when {
+ this == null || TransformationClassLoader.doNotTransform(this.javaClass.name) -> 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<*> = Class.forName(desc.name, true, loader)
}
\ No newline at end of file
diff --git a/src/main/java/org/jetbrains/kotlinx/lincheck/runner/Runner.java b/src/main/java/org/jetbrains/kotlinx/lincheck/runner/Runner.java
index 5aeb9b856..ec2679dbb 100644
--- a/src/main/java/org/jetbrains/kotlinx/lincheck/runner/Runner.java
+++ b/src/main/java/org/jetbrains/kotlinx/lincheck/runner/Runner.java
@@ -32,6 +32,8 @@
import java.util.*;
import java.util.concurrent.atomic.*;
+import static org.jetbrains.kotlinx.lincheck.UtilsKt.convertForLoader;
+
/**
* Runner determines how to run your concurrent test. In order to support techniques
* like fibers, it may require code transformation, so {@link #needsTransformation()}
@@ -47,9 +49,9 @@ public abstract class Runner {
protected final AtomicInteger completedOrSuspendedThreads = new AtomicInteger(0);
protected Runner(Strategy strategy, Class> testClass, List validationFunctions) {
- this.scenario = strategy.getScenario();
this.classLoader = (this.needsTransformation() || strategy.needsTransformation()) ?
new TransformationClassLoader(strategy, this) : new ExecutionClassLoader();
+ this.scenario = convertForLoader(strategy.getScenario(), classLoader);
this.testClass = loadClass(testClass.getTypeName());
this.validationFunctions = validationFunctions;
}
diff --git a/src/test/java/org/jetbrains/kotlinx/lincheck/test/ThreadDumpTest.kt b/src/test/java/org/jetbrains/kotlinx/lincheck/test/ThreadDumpTest.kt
index 1db912bc8..9aad36995 100644
--- a/src/test/java/org/jetbrains/kotlinx/lincheck/test/ThreadDumpTest.kt
+++ b/src/test/java/org/jetbrains/kotlinx/lincheck/test/ThreadDumpTest.kt
@@ -41,8 +41,8 @@ class ThreadDumpTest {
.invocationsPerIteration(1)
.invocationTimeout(100)
val failure = options.checkImpl(DeadlockOnSynchronizedTest::class.java)
- check(failure is DeadlockWithDumpFailure)
- check(failure.threadDump.size == 2) { "thread dump for 2 threads expected, but for ${failure.threadDump.size} threads found"}
+ check(failure is DeadlockWithDumpFailure) { "${DeadlockWithDumpFailure::class.simpleName} was expected but ${failure?.javaClass} was obtained"}
+ check(failure.threadDump.size == 2) { "thread dump for 2 threads expected, but for ${failure.threadDump.size} threads was detected"}
}
}
}
diff --git a/src/test/java/org/jetbrains/kotlinx/lincheck/test/transformation/SerializableValueTests.kt b/src/test/java/org/jetbrains/kotlinx/lincheck/test/transformation/SerializableValueTests.kt
new file mode 100644
index 000000000..5de3a04d7
--- /dev/null
+++ b/src/test/java/org/jetbrains/kotlinx/lincheck/test/transformation/SerializableValueTests.kt
@@ -0,0 +1,70 @@
+/*-
+ * #%L
+ * Lincheck
+ * %%
+ * Copyright (C) 2019 - 2020 JetBrains s.r.o.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Lesser Public License for more details.
+ *
+ * You should have received a copy of the GNU General Lesser Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package org.jetbrains.kotlinx.lincheck.test.transformation
+
+import org.jetbrains.kotlinx.lincheck.Options
+import org.jetbrains.kotlinx.lincheck.annotations.*
+import org.jetbrains.kotlinx.lincheck.paramgen.ParameterGenerator
+import org.jetbrains.kotlinx.lincheck.test.AbstractLincheckTest
+import java.io.Serializable
+import java.util.concurrent.atomic.*
+
+class SerializableResultTest : AbstractLincheckTest() {
+ private val counter = AtomicReference(ValueHolder(0))
+
+ @Operation
+ fun getAndSet(key: Int) = counter.getAndSet(ValueHolder(key))
+
+ override fun extractState(): Any = counter.get().value
+
+ override fun > O.customize() {
+ iterations(1)
+ actorsBefore(0)
+ actorsAfter(0)
+ }
+}
+
+
+@Param(name = "key", gen = ValueHolderGen::class)
+class SerializableParameterTest : AbstractLincheckTest() {
+ private val counter = AtomicInteger(0)
+
+ @Operation
+ fun operation(@Param(name = "key") key: ValueHolder): Int = counter.addAndGet(key.value)
+
+ override fun > O.customize() {
+ iterations(1)
+ actorsBefore(0)
+ actorsAfter(0)
+ }
+
+ override fun extractState(): Any = counter.get()
+}
+
+
+class ValueHolder(val value: Int) : Serializable
+
+class ValueHolderGen(conf: String) : ParameterGenerator {
+ override fun generate(): ValueHolder {
+ return listOf(ValueHolder(1), ValueHolder(2)).random()
+ }
+}
diff --git a/src/test/java/org/jetbrains/kotlinx/lincheck/test/transformation/TransformedExceptionTests.kt b/src/test/java/org/jetbrains/kotlinx/lincheck/test/transformation/TransformedExceptionTests.kt
new file mode 100644
index 000000000..197e928be
--- /dev/null
+++ b/src/test/java/org/jetbrains/kotlinx/lincheck/test/transformation/TransformedExceptionTests.kt
@@ -0,0 +1,56 @@
+/*-
+ * #%L
+ * Lincheck
+ * %%
+ * Copyright (C) 2019 - 2020 JetBrains s.r.o.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Lesser Public License for more details.
+ *
+ * You should have received a copy of the GNU General Lesser Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package org.jetbrains.kotlinx.lincheck.test.transformation
+
+import org.jetbrains.kotlinx.lincheck.Options
+import org.jetbrains.kotlinx.lincheck.annotations.Operation
+import org.jetbrains.kotlinx.lincheck.strategy.*
+import org.jetbrains.kotlinx.lincheck.test.AbstractLincheckTest
+
+class ExpectedTransformedExceptionTest : AbstractLincheckTest() {
+ @Operation(handleExceptionsAsResult = [CustomException::class])
+ fun operation(): Unit = throw CustomException()
+
+ override fun > O.customize() {
+ iterations(1)
+ }
+
+ override fun extractState(): Any = 0 // constant state
+}
+
+class UnexpectedTransformedExceptionTest : AbstractLincheckTest(UnexpectedExceptionFailure::class) {
+ @Volatile
+ var throwException = false
+
+ @Operation
+ fun operation(): Int {
+ throwException = true
+ throwException = false
+ if (throwException)
+ throw CustomException()
+ return 0
+ }
+
+ override fun extractState(): Any = 0 // constant state
+}
+
+internal class CustomException : Throwable()