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()