Skip to content

Commit

Permalink
Support Serializable classes as operation parameters and results (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
alefedor authored Aug 27, 2020
1 parent 0d13dae commit d6804d6
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 34 deletions.
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

0 comments on commit d6804d6

Please sign in to comment.