Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supported serializable classes as actor parameters and results #46

Merged
merged 5 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)



Expand Down Expand Up @@ -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.
Expand Down
30 changes: 28 additions & 2 deletions src/main/java/org/jetbrains/kotlinx/lincheck/Result.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jetbrains.kotlinx.lincheck

import java.io.*
import kotlin.coroutines.*

/*
Expand Down Expand Up @@ -27,7 +28,6 @@ import kotlin.coroutines.*
/**
* The instance of this class represents a result of actor invocation.
*

* <p> 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.
*
Expand All @@ -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<T>) 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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,11 +66,11 @@ public TransformationClassLoader(Function<ClassVisitor, ClassVisitor> 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.") ||
Expand Down
56 changes: 50 additions & 6 deletions src/main/java/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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) {
Expand All @@ -99,7 +101,7 @@ internal fun executeActor(
}

internal inline fun executeValidationFunctions(instance: Any, validationFunctions: List<Method>,
onError: (functionName: String, exception: Throwable) -> Unit) {
onError: (functionName: String, exception: Throwable) -> Unit) {
for (f in validationFunctions) {
val validationException = executeValidationFunction(instance, f)
if (validationException != null) {
Expand All @@ -126,12 +128,20 @@ private val methodsCache = WeakHashMap<Class<*>, WeakHashMap<Method, WeakReferen
private fun getMethod(instance: Any, method: Method): Method {
val methods = methodsCache.computeIfAbsent(instance.javaClass) { WeakHashMap() }
return methods[method]?.get() ?: run {
val m = instance.javaClass.getMethod(method.name, *method.parameterTypes)
val m = instance.javaClass.getMethod(method.name, method.parameterTypes)
methods[method] = WeakReference(m)
m
}
}

/**
* Finds a method corresponding to [name] and [parameterTypes] ignoring difference in loaders for [parameterTypes].
*/
private fun Class<out Any>.getMethod(name: String, parameterTypes: Array<Class<out Any>>): 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.
*
Expand Down Expand Up @@ -163,7 +173,6 @@ private fun kotlin.Result<Any?>.toLinCheckResult(wasSuspended: Boolean) =
}
} else ExceptionResult.create(exceptionOrNull()!!.let { it::class.java }, wasSuspended)


inline fun <R> Throwable.catch(vararg exceptions: Class<*>, block: () -> R): R {
if (exceptions.any { this::class.java.isAssignableFrom(it) }) {
return block()
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Expand All @@ -47,9 +49,9 @@ public abstract class Runner {
protected final AtomicInteger completedOrSuspendedThreads = new AtomicInteger(0);

protected Runner(Strategy strategy, Class<?> testClass, List<Method> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
* <http://www.gnu.org/licenses/lgpl-3.0.html>.
* #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 : Options<O, *>> 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 : Options<O, *>> 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<ValueHolder> {
override fun generate(): ValueHolder {
return listOf(ValueHolder(1), ValueHolder(2)).random()
}
}
Loading