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

At most one validation function is allowed #243

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ abstract class CTestConfiguration(
val customScenarios: List<ExecutionScenario>
) {
abstract fun createStrategy(
testClass: Class<*>, scenario: ExecutionScenario, validationFunctions: List<Actor>,
testClass: Class<*>, scenario: ExecutionScenario, validationFunction: Actor?,
stateRepresentationMethod: Method?, verifier: Verifier
): Strategy

Expand Down
83 changes: 50 additions & 33 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.jetbrains.kotlinx.lincheck;

import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlinx.lincheck.annotations.*;
import org.jetbrains.kotlinx.lincheck.execution.*;
import org.jetbrains.kotlinx.lincheck.paramgen.*;
Expand All @@ -30,17 +31,18 @@ public class CTestStructure {
public final List<ActorGenerator> actorGenerators;
public final List<ParameterGenerator<?>> parameterGenerators;
public final List<OperationGroup> operationGroups;
public final List<Actor> validationFunctions;
@Nullable
public final Actor validationFunction;
public final Method stateRepresentation;

public final RandomProvider randomProvider;

private CTestStructure(List<ActorGenerator> actorGenerators, List<ParameterGenerator<?>> parameterGenerators, List<OperationGroup> operationGroups,
List<Actor> validationFunctions, Method stateRepresentation, RandomProvider randomProvider) {
@Nullable Actor validationFunction, Method stateRepresentation, RandomProvider randomProvider) {
this.actorGenerators = actorGenerators;
this.parameterGenerators = parameterGenerators;
this.operationGroups = operationGroups;
this.validationFunctions = validationFunctions;
this.validationFunction = validationFunction;
this.stateRepresentation = stateRepresentation;
this.randomProvider = randomProvider;
}
Expand All @@ -49,39 +51,21 @@ private CTestStructure(List<ActorGenerator> actorGenerators, List<ParameterGener
* Constructs {@link CTestStructure} for the specified test class.
*/
public static CTestStructure getFromTestClass(Class<?> testClass) {
Map<String, OperationGroup> groupConfigs = new HashMap<>();
List<ActorGenerator> actorGenerators = new ArrayList<>();
List<Actor> validationFunctions = new ArrayList<>();
List<Method> stateRepresentations = new ArrayList<>();
Class<?> clazz = testClass;
RandomProvider randomProvider = new RandomProvider();
Map<Class<?>, ParameterGenerator<?>> parameterGeneratorsMap = new HashMap<>();
CTestStructureBuilder testStructureBuilder = new CTestStructureBuilder();

while (clazz != null) {
readTestStructureFromClass(clazz, groupConfigs, actorGenerators, parameterGeneratorsMap, validationFunctions, stateRepresentations, randomProvider);
readTestStructureFromClass(clazz, testStructureBuilder, randomProvider);
clazz = clazz.getSuperclass();
}
if (stateRepresentations.size() > 1) {
throw new IllegalStateException("Several functions marked with " + StateRepresentation.class.getSimpleName() +
" were found, while at most one should be specified: " +
stateRepresentations.stream().map(Method::getName).collect(Collectors.joining(", ")));
}
Method stateRepresentation = null;
if (!stateRepresentations.isEmpty())
stateRepresentation = stateRepresentations.get(0);
// Create StressCTest class configuration
List<ParameterGenerator<?>> parameterGenerators = new ArrayList<>(parameterGeneratorsMap.values());

return new CTestStructure(actorGenerators, parameterGenerators, new ArrayList<>(groupConfigs.values()), validationFunctions, stateRepresentation, randomProvider);
return testStructureBuilder.build(randomProvider);
}

private static void readTestStructureFromClass(
Class<?> clazz,
Map<String, OperationGroup> groupConfigs,
List<ActorGenerator> actorGenerators,
Map<Class<?>, ParameterGenerator<?>> parameterGeneratorsMap,
List<Actor> validationFunctions,
List<Method> stateRepresentations,
CTestStructureBuilder testStructureBuilder,
RandomProvider randomProvider
) {
// Read named parameter generators (declared for class)
Expand All @@ -90,7 +74,7 @@ private static void readTestStructureFromClass(
Map<Class<?>, ParameterGenerator<?>> defaultGens = createDefaultGenerators(randomProvider);
// Read group configurations
for (OpGroupConfig opGroupConfigAnn: clazz.getAnnotationsByType(OpGroupConfig.class)) {
groupConfigs.put(opGroupConfigAnn.name(), new OperationGroup(opGroupConfigAnn.name(),
testStructureBuilder.groupConfigs.put(opGroupConfigAnn.name(), new OperationGroup(opGroupConfigAnn.name(),
opGroupConfigAnn.nonParallel()));
}
// Process class methods
Expand All @@ -110,43 +94,53 @@ private static void readTestStructureFromClass(
String nameInOperation = opAnn.params().length > 0 ? opAnn.params()[i] : null;
Parameter parameter = m.getParameters()[i];
ParameterGenerator<?> parameterGenerator = getOrCreateGenerator(m, parameter, nameInOperation, namedGens, defaultGens, randomProvider);
parameterGeneratorsMap.putIfAbsent(parameter.getType(), parameterGenerator);
testStructureBuilder.parameterGeneratorsMap.putIfAbsent(parameter.getType(), parameterGenerator);
gens.add(parameterGenerator);
}
ActorGenerator actorGenerator = new ActorGenerator(m, gens, opAnn.runOnce(),
opAnn.cancellableOnSuspension(), opAnn.allowExtraSuspension(), opAnn.blocking(), opAnn.causesBlocking(),
opAnn.promptCancellation());
actorGenerators.add(actorGenerator);
testStructureBuilder.actorGenerators.add(actorGenerator);
// Get list of groups and add this operation to specified ones
String opGroup = opAnn.group();
if (!opGroup.isEmpty()) {
OperationGroup operationGroup = groupConfigs.get(opGroup);
OperationGroup operationGroup = testStructureBuilder.groupConfigs.get(opGroup);
if (operationGroup == null)
throw new IllegalStateException("Operation group " + opGroup + " is not configured");
operationGroup.actors.add(actorGenerator);
}
String opNonParallelGroupName = opAnn.nonParallelGroup();
if (!opNonParallelGroupName.isEmpty()) { // is `nonParallelGroup` specified?
groupConfigs.computeIfAbsent(opNonParallelGroupName, name -> new OperationGroup(name, true));
groupConfigs.get(opNonParallelGroupName).actors.add(actorGenerator);
testStructureBuilder.groupConfigs.computeIfAbsent(opNonParallelGroupName, name -> new OperationGroup(name, true));
testStructureBuilder.groupConfigs.get(opNonParallelGroupName).actors.add(actorGenerator);
}
}
if (m.isAnnotationPresent(Validate.class)) {
if (m.getParameterCount() != 0)
throw new IllegalStateException("Validation function " + m.getName() + " should not have parameters");
validationFunctions.add(new Actor(m, Collections.emptyList()));
if (testStructureBuilder.validationFunction == null) {
testStructureBuilder.validationFunction = new Actor(m, Collections.emptyList());
} else {
throw new IllegalStateException("You can't have more than one validation function.\n" +
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
"@Validation annotation is present here: " + getMethodSignature(testStructureBuilder.validationFunction.getMethod()) +
", and here: " + getMethodSignature(m));
}
}

if (m.isAnnotationPresent(StateRepresentation.class)) {
if (m.getParameterCount() != 0)
throw new IllegalStateException("State representation function " + m.getName() + " should not have parameters");
if (m.getReturnType() != String.class)
throw new IllegalStateException("State representation function " + m.getName() + " should have String return type");
stateRepresentations.add(m);
testStructureBuilder.stateRepresentations.add(m);
}
}
}

public static String getMethodSignature(Method m) {
return m.getDeclaringClass().getName() + "." + m.getName();
}

private static Map<String, ParameterGenerator<?>> createNamedGens(Class<?> clazz, RandomProvider randomProvider) {
Map<String, ParameterGenerator<?>> namedGens = new HashMap<>();
// Traverse all operations to determine named EnumGens types
Expand Down Expand Up @@ -305,6 +299,29 @@ private static ParameterGenerator<?> checkAndGetNamedGenerator(Map<String, Param
return Objects.requireNonNull(namedGens.get(name), "Unknown generator name: \"" + name + "\"");
}

private static class CTestStructureBuilder {
Map<String, OperationGroup> groupConfigs = new HashMap<>();
List<ActorGenerator> actorGenerators = new ArrayList<>();
Actor validationFunction = null;
List<Method> stateRepresentations = new ArrayList<>();
Map<Class<?>, ParameterGenerator<?>> parameterGeneratorsMap = new HashMap<>();

public CTestStructure build(RandomProvider randomProvider) {
if (stateRepresentations.size() > 1) {
throw new IllegalStateException("Several functions marked with " + StateRepresentation.class.getSimpleName() +
" were found, while at most one should be specified: " +
stateRepresentations.stream().map(Method::getName).collect(Collectors.joining(", ")));
}
Method stateRepresentation = null;
if (!stateRepresentations.isEmpty())
stateRepresentation = stateRepresentations.get(0);
// Create StressCTest class configuration
List<ParameterGenerator<?>> parameterGenerators = new ArrayList<>(parameterGeneratorsMap.values());

return new CTestStructure(actorGenerators, parameterGenerators, new ArrayList<>(groupConfigs.values()), validationFunction, stateRepresentation, randomProvider);
}
}

public static class OperationGroup {
public final String name;
public final boolean nonParallel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ class DSLScenarioBuilder {
/**
* Constructs a new [scenario][ExecutionScenario] according to
* the specified [initial], [parallel], and [post] parts.
* As validation functions can be found only after test class scan, we temporarily set it to `null`.
* As a validation function can be found only after test class scan, we temporarily set it to `null`.
*/
fun buildScenario() = ExecutionScenario(initial, parallel, post, validationFunctions = null)
fun buildScenario() = ExecutionScenario(initial, parallel, post, validationFunction = null)
}

@DslMarker
Expand Down
4 changes: 2 additions & 2 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) {
// Currently, we extract validation functions from testClass structure, so for custom scenarios declared
// with DSL, we have to set up it when testClass is scanned
testConfigurations.forEach { cTestConfiguration ->
cTestConfiguration.customScenarios.forEach { it.validationFunctions = testStructure.validationFunctions }
cTestConfiguration.customScenarios.forEach { it.validationFunction = testStructure.validationFunction }
}
}

Expand Down Expand Up @@ -120,7 +120,7 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) {
testCfg.createStrategy(
testClass = testClass,
scenario = this,
validationFunctions = testStructure.validationFunctions,
validationFunction = testStructure.validationFunction,
stateRepresentationMethod = testStructure.stateRepresentation,
verifier = verifier
).run()
Expand Down
22 changes: 11 additions & 11 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ internal class TableLayout(
*
* @see columnsToString
*/
fun <T> StringBuilder.appendToFirstColumn(data: List<T>, transform: ((T) -> String)? = null) = apply {
val columns = listOf(data) + List(columnWidths.size - 1) { emptyList() }
appendColumns(columns, columnWidths, transform)
fun <T> StringBuilder.appendToFirstColumn(data: T) = apply {
val columns = listOf(listOf(data)) + List(columnWidths.size - 1) { emptyList() }
appendColumns(columns, columnWidths, transform = null)
}

/**
Expand Down Expand Up @@ -211,11 +211,11 @@ internal fun ExecutionLayout(
initPart: List<String>,
parallelPart: List<List<String>>,
postPart: List<String>,
validationPart: List<String>
validationFunctionName: String?
): TableLayout {
val size = parallelPart.size
val threadHeaders = (0 until size).map { "Thread ${it + 1}" }
val firstThreadNonParallelParts = initPart + postPart + validationPart
val firstThreadNonParallelParts = initPart + postPart + (validationFunctionName?.let { listOf(it) } ?: emptyList())
val columnsContent = parallelPart.map { it.toMutableList() }.toMutableList()

if (columnsContent.isNotEmpty()) {
Expand Down Expand Up @@ -259,8 +259,8 @@ internal fun StringBuilder.appendExecutionScenario(
val initPart = scenario.initExecution.map(Actor::toString)
val postPart = scenario.postExecution.map(Actor::toString)
val parallelPart = scenario.parallelExecution.map { it.map(Actor::toString) }
val validationPart = if (showValidationFunctions) scenario.validationFunctions?.map { "${it.method.name}()" } ?: emptyList() else emptyList()
with(ExecutionLayout(initPart, parallelPart, postPart, validationPart)) {
val validationFunctionName = if (showValidationFunctions) scenario.validationFunction?.let { "${it.method.name}()" } else null
with(ExecutionLayout(initPart, parallelPart, postPart, validationFunctionName)) {
appendSeparatorLine()
appendHeader()
appendSeparatorLine()
Expand All @@ -274,8 +274,8 @@ internal fun StringBuilder.appendExecutionScenario(
appendColumn(0, postPart)
appendSeparatorLine()
}
if (validationPart.isNotEmpty()) {
appendToFirstColumn(validationPart)
if (validationFunctionName != null) {
appendToFirstColumn(validationFunctionName)
appendSeparatorLine()
}
}
Expand Down Expand Up @@ -344,7 +344,7 @@ internal fun StringBuilder.appendExecutionScenarioWithResults(
ActorWithResult(actor, resultWithClock.result, exceptionStackTraces, clock = resultWithClock.clockOnStart).toString()
}
}
with(ExecutionLayout(initPart, parallelPart, postPart, validationPart = emptyList())) {
with(ExecutionLayout(initPart, parallelPart, postPart, validationFunctionName = null)) {
appendSeparatorLine()
appendHeader()
appendSeparatorLine()
Expand Down Expand Up @@ -609,7 +609,7 @@ private fun StringBuilder.appendHints(hints: List<String>) {
}

private fun StringBuilder.appendValidationFailure(failure: ValidationFailure): StringBuilder {
appendLine("= Validation function ${failure.functionName} has failed =")
appendLine("= Validation function ${failure.validationFunctionName} has failed =")
appendExecutionScenario(failure.scenario, showValidationFunctions = true)
appendln()
appendln()
Expand Down
3 changes: 1 addition & 2 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ package org.jetbrains.kotlinx.lincheck
import kotlinx.coroutines.*
import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.strategy.Strategy
import org.jetbrains.kotlinx.lincheck.strategy.managed.*
import org.jetbrains.kotlinx.lincheck.verifier.*
import org.objectweb.asm.*
Expand Down Expand Up @@ -192,7 +191,7 @@ internal fun ExecutionScenario.convertForLoader(loader: ClassLoader) = Execution
postExecution.map {
it.convertForLoader(loader)
},
validationFunctions = validationFunctions
validationFunction = validationFunction
)

private fun Actor.convertForLoader(loader: ClassLoader): Actor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import kotlin.annotation.AnnotationTarget.*
/**
* It is possible to check the data structure consistency at the end of the execution
* by specifying a validation function that is called at the end of each scenario.
* At most one validation function is allowed.
*
* The validation functions should be marked with this annotation,
* have no arguments, and not modify the data structure state.
* The validation function should be marked with this annotation and have no arguments.
* In case the testing data structure is in an invalid state, they should throw an exception.
* ([AssertionError] or [IllegalStateException] are the preferable ones).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
package org.jetbrains.kotlinx.lincheck.execution

import org.jetbrains.kotlinx.lincheck.*
import java.lang.reflect.Method

/**
* This class represents an execution scenario.
Expand Down Expand Up @@ -45,10 +44,10 @@ class ExecutionScenario(
*/
val postExecution: List<Actor>,
/**
* Validation functions that will be called after scenario execution.
* Is `var` as in case of custom scenario validation functions are found only after test class scan.
* Validation function that will be called after scenario execution.
* Is `var` as in case of custom scenario validation function is found only after test class scan.
*/
var validationFunctions: List<Actor>?
var validationFunction: Actor?
) {

/**
Expand Down Expand Up @@ -134,7 +133,7 @@ fun ExecutionScenario.copy() = ExecutionScenario(
ArrayList(initExecution),
parallelExecution.map { ArrayList(it) },
ArrayList(postExecution),
validationFunctions
validationFunction
)

/**
Expand Down Expand Up @@ -165,7 +164,7 @@ fun ExecutionScenario.tryMinimize(threadId: Int, actorId: Int): ExecutionScenari
}
}
.filter { it.isNotEmpty() }
.splitIntoParts(initPartSize, postPartSize, validationFunctions)
.splitIntoParts(initPartSize, postPartSize, validationFunction)
.takeIf { it.isValid }
}

Expand All @@ -178,10 +177,10 @@ fun ExecutionScenario.tryMinimize(threadId: Int, actorId: Int): ExecutionScenari
* @param postPartSize the size of the post part of the execution.
* @return execution scenario with separate init, post, and parallel parts.
*/
private fun List<List<Actor>>.splitIntoParts(initPartSize: Int, postPartSize: Int, validationFunctions: List<Actor>?): ExecutionScenario {
private fun List<List<Actor>>.splitIntoParts(initPartSize: Int, postPartSize: Int, validationFunction: Actor?): ExecutionScenario {
// empty scenario case
if (isEmpty())
return ExecutionScenario(listOf(), listOf(), listOf(), validationFunctions)
return ExecutionScenario(listOf(), listOf(), listOf(), validationFunction)
// get potential init and post parts
val firstThreadSize = get(0).size
val initExecution = get(0).subList(0, initPartSize)
Expand All @@ -199,9 +198,9 @@ private fun List<List<Actor>>.splitIntoParts(initPartSize: Int, postPartSize: In
// single-thread scenario is not split into init/post parts
if (parallelExecution.size == 1) {
val threadExecution = initExecution + parallelExecution[0] + postExecution
return ExecutionScenario(listOf(), listOf(threadExecution), listOf(), validationFunctions)
return ExecutionScenario(listOf(), listOf(threadExecution), listOf(), validationFunction)
}
return ExecutionScenario(initExecution, parallelExecution, postExecution, validationFunctions)
return ExecutionScenario(initExecution, parallelExecution, postExecution, validationFunction)
}

const val INIT_THREAD_ID = 0
Expand Down
Loading