diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..c361f379a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cpp/libcuckoo/libcuckoo"] + path = cpp/libcuckoo/libcuckoo + url = https://github.com/efficient/libcuckoo diff --git a/build.gradle.kts b/build.gradle.kts index ca12958ea..c8967d4fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,11 @@ import org.gradle.jvm.tasks.Jar +allprojects { + ext { + set("hostManager", org.jetbrains.kotlin.konan.target.HostManager()) + } +} + // atomicfu buildscript { val atomicfuVersion: String by project @@ -9,6 +15,8 @@ buildscript { } apply(plugin = "kotlinx-atomicfu") +apply(from = rootProject.file("gradle/native-targets.gradle")) + plugins { java kotlin("multiplatform") @@ -36,6 +44,34 @@ kotlin { } sourceSets { + val commonMain by getting { + kotlin.srcDir("src/common/main") + + val kotlinVersion: String by project + val kotlinxCoroutinesVersion: String by project + val asmVersion: String by project + val reflectionsVersion: String by project + dependencies { + api("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") + api("org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion") + api("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + api("org.ow2.asm:asm-commons:$asmVersion") + api("org.ow2.asm:asm-util:$asmVersion") + api("org.reflections:reflections:$reflectionsVersion") + } + } + + val commonTest by getting { + kotlin.srcDir("src/common/test") + + val kotlinVersion: String by project + dependencies { + implementation(kotlin("test-common", kotlinVersion)) + implementation(kotlin("test-annotations-common", kotlinVersion)) + } + } + val jvmMain by getting { kotlin.srcDir("src/jvm/main") @@ -80,6 +116,11 @@ sourceSets.test { java.srcDirs("src/jvm/test") } +tasks.wrapper { + gradleVersion = "6.7.1" + distributionType = Wrapper.DistributionType.ALL +} + tasks { // empty xxx-javadoc.jar register("javadocJar") { @@ -89,7 +130,7 @@ tasks { withType { maxParallelForks = 1 jvmArgs("--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", - "--add-exports", "java.base/jdk.internal.util=ALL-UNNAMED") + "--add-exports", "java.base/jdk.internal.util=ALL-UNNAMED") } withType { @@ -104,6 +145,28 @@ tasks { } } +tasks.register("cppTest") { + dependsOn("linkNative") + doLast { + exec { + workingDir("cpp") + commandLine("mkdir", "-p", "build") + } + exec { + workingDir("cpp/build") + commandLine("cmake", "..") + } + exec { + workingDir("cpp/build") + commandLine("make") + } + exec { + workingDir("cpp/build") + commandLine("ctest", "--output-on-failure") + } + } +} + publishing { publications.withType { // add empty javadoc diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 000000000..f1413531d --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,97 @@ +cmake_minimum_required(VERSION 3.16) +project(lincheck_test) + +set(CMAKE_CXX_STANDARD 17) + +# add lincheck shared library +add_library(lincheck_library SHARED IMPORTED GLOBAL) +if(EXISTS "${PROJECT_SOURCE_DIR}/../build/bin/native/releaseShared/libnative.so") #linux .so + set_target_properties(lincheck_library PROPERTIES IMPORTED_LOCATION "${PROJECT_SOURCE_DIR}/../build/bin/native/releaseShared/libnative.so") +else() # mac .dylib + set_target_properties(lincheck_library PROPERTIES IMPORTED_LOCATION "${PROJECT_SOURCE_DIR}/../build/bin/native/releaseShared/libnative.dylib") +endif() + +# add gtest +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +# add libcds +FetchContent_Declare( + libcds + GIT_REPOSITORY https://github.com/khizmax/libcds + GIT_TAG master + GIT_SHALLOW TRUE +) + +FetchContent_GetProperties(libcds) +if (NOT libcds_POPULATED) + FetchContent_Populate(libcds) + add_subdirectory(${libcds_SOURCE_DIR} ${libcds_BINARY_DIR}) +endif () + +# add folly +FetchContent_Declare( + folly + GIT_REPOSITORY https://github.com/facebook/folly + GIT_TAG master + GIT_SHALLOW TRUE +) + +FetchContent_GetProperties(folly) +if (NOT folly_POPULATED) + FetchContent_Populate(folly) + add_subdirectory(${folly_SOURCE_DIR} ${folly_BINARY_DIR}) +endif () + +# add libcuckoo +FetchContent_Declare( + libcuckoo + GIT_REPOSITORY https://github.com/efficient/libcuckoo + GIT_TAG master + GIT_SHALLOW TRUE +) + +FetchContent_GetProperties(libcuckoo) +if (NOT libcuckoo_POPULATED) + FetchContent_Populate(libcuckoo) + add_subdirectory(${libcuckoo_SOURCE_DIR} ${libcuckoo_BINARY_DIR}) + include_directories(${libcuckoo_SOURCE_DIR}) +endif () + +# add boost +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +find_package(Boost REQUIRED COMPONENTS thread program_options) + +add_executable( + tests + counter_test.cpp + libcds_test.cpp + libcuckoo_test.cpp + folly_test.cpp + boost_lockfree_test.cpp +) + +enable_testing() + +target_link_libraries(tests gtest_main) +target_link_libraries(tests gmock_main) +target_link_libraries(tests lincheck_library) +target_link_libraries(tests cds) +target_link_libraries(tests libcuckoo) +target_link_libraries(tests folly) +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +include_directories(${Boost_INCLUDE_DIR} ${BOOST_LOCKFREE_DIR}) +target_link_libraries(tests ${Boost_LIBRARIES}) + +include(GoogleTest) +gtest_discover_tests(tests) diff --git a/cpp/README.MD b/cpp/README.MD new file mode 100644 index 000000000..e976cf1c5 --- /dev/null +++ b/cpp/README.MD @@ -0,0 +1,8 @@ +In the kotlinx-lincheck directory run `./gradlew linkNative`. It generates `build/bin/native/debugShared` and `build/bin/native/releaseShared` + +Run this as a CMake application +1. `mkdir build` +2. `cd build` +3. `cmake ..` +4. `make` +5. `./lincheck_test` diff --git a/cpp/boost_lockfree_test.cpp b/cpp/boost_lockfree_test.cpp new file mode 100644 index 000000000..34c1c77a5 --- /dev/null +++ b/cpp/boost_lockfree_test.cpp @@ -0,0 +1,228 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "lincheck.h" +#include "lincheck_functions.h" +#include +#include + +#include +#include +#include + +class SequentialQueueBoost { +public: + std::queue q; + + bool push(int value) { + q.push(value); + return true; + } + + std::pair pop() { + int val = 0; + if(!q.empty()) { + val = q.front(); + q.pop(); + return {true, val}; + } else { + return {false, val}; + } + } +}; + +class ConcurrentQueueBoost { +public: + boost::lockfree::queue q = boost::lockfree::queue(1); + + bool push(int value) { + return q.push(value); + } + + std::pair pop() { + int val = 0; + bool success = q.pop(val); + return {success, success ? val : 0}; + } +}; + +class ConcurrentSPSCQueueBoost { +public: + boost::lockfree::spsc_queue q = boost::lockfree::spsc_queue(100); // fixed-size ring buffer + + bool push(int value) { + return q.push(value); + } + + std::pair pop() { + int val = 0; + bool success = q.pop(val); + return {success, success ? val : 0}; + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(SequentialQueueBoost &s) const noexcept { + std::vector vec; + while(!s.q.empty()) { + vec.push_back(s.q.front()); + s.q.pop(); + } + for (auto it : vec) { + s.q.push(it); + } + return Lincheck::hash>()(vec); + } +}; + +bool operator==(const SequentialQueueBoost &a, const SequentialQueueBoost &b) { + return a.q == b.q; +} + +class SequentialStackBoost { +public: + std::stack s; + + bool push(int value) { + s.push(value); + return true; + } + + std::pair pop() { + int val = 0; + if(!s.empty()) { + val = s.top(); + s.pop(); + return {true, val}; + } else { + return {false, val}; + } + } + + bool empty() { + return s.empty(); + } +}; + +class ConcurrentStackBoost { +public: + boost::lockfree::stack s = boost::lockfree::stack(1); + + bool push(int value) { + return s.push(value); + } + + std::pair pop() { + int val = 0; + bool success = s.pop(val); + return {success, success ? val : 0}; + } + + bool empty() { + return s.empty(); + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(SequentialStackBoost &s) const noexcept { + std::vector vec; + while(!s.s.empty()) { + vec.push_back(s.s.top()); + s.s.pop(); + } + reverse(vec.begin(), vec.end()); + for (auto it : vec) { + s.s.push(it); + } + return Lincheck::hash>()(vec); + } +}; + +bool operator==(const SequentialStackBoost &a, const SequentialStackBoost &b) { + return a.s == b.s; +} + +using namespace Lincheck; + +TEST(BoostLockfreeTest, BadSequentialQueueTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(3); + + conf.operation("push"); + conf.operation, &SequentialQueueBoost::pop, &SequentialQueueBoost::pop>("pop"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(BoostLockfreeTest, BadSequentialStackTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(3); + + conf.operation("push"); + conf.operation, &SequentialStackBoost::pop, &SequentialStackBoost::pop>("pop"); + conf.operation("empty"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(BoostLockfreeTest, QueueTest) { + LincheckConfiguration conf; + conf.iterations(100); + conf.invocationsPerIteration(500); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(3); + conf.actorsAfter(10); + + conf.operation("push"); + conf.operation, &ConcurrentQueueBoost::pop, &SequentialQueueBoost::pop>("pop"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(BoostLockfreeTest, StackTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(3); + + conf.operation("push"); + conf.operation, &ConcurrentStackBoost::pop, &SequentialStackBoost::pop>("pop"); + conf.operation("empty"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(BoostLockfreeTest, BadSPSCQueueTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(3); + + conf.operation("push"); + conf.operation, &ConcurrentSPSCQueueBoost::pop, &SequentialQueueBoost::pop>("pop", "popNonParallelGroup"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(BoostLockfreeTest, SPSCQueueTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(2); + conf.actorsPerThread(3); + + conf.operation("push", "pushNonParallelGroup"); + conf.operation, &ConcurrentSPSCQueueBoost::pop, &SequentialQueueBoost::pop>("pop", "popNonParallelGroup"); + ASSERT_EQ(conf.runTest(false), ""); +} \ No newline at end of file diff --git a/cpp/counter_test.cpp b/cpp/counter_test.cpp new file mode 100644 index 000000000..4a84d7954 --- /dev/null +++ b/cpp/counter_test.cpp @@ -0,0 +1,184 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "lincheck.h" + +class ComplexArg { +public: + int value; + + ComplexArg() { + value = 29; + } +}; + +template<> +struct Lincheck::to_string { + std::string operator()(const ComplexArg &arg) const noexcept { + return std::to_string(arg.value); + } +}; + +template<> +class Lincheck::ParameterGenerator { +public: + ComplexArg generate() { + return ComplexArg(); + } +}; + +class Counter { +public: + int sharedState = 0; + std::atomic_int sharedAtomicState = 0; + int counter_validate_invocations = 0; + + int inc() { + auto ans = ++sharedState; + return ans; + } + + int dec() { + auto ans = --sharedState; + return ans; + } + + int add(int value) { + auto ans = sharedState += value; + return ans; + } + + int double_op(int value1, ComplexArg value2) { + return 0; + } + + int atomic_inc() { + return ++sharedAtomicState; + } + + int atomic_dec() { + return --sharedAtomicState; + } + + int atomic_add(int value) { + //std::cout << "atomic_add " << value << " started" << std::endl; + auto ans = sharedAtomicState += value; + //std::cout << "atomic_add " << value << " finished" << std::endl; + return ans; + } + + void validate_no_error() {} + + void validate_with_error() { + counter_validate_invocations++; + if (counter_validate_invocations == 5) { + throw std::runtime_error("ValidationRuntimeError"); + } + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(Counter const &s) const noexcept { + return Lincheck::hash()(s.sharedAtomicState); + } +}; + +bool operator==(const Counter &a, const Counter &b) { + return a.sharedAtomicState == b.sharedAtomicState; +} + +using namespace Lincheck; +using ::testing::HasSubstr; + +TEST(CounterTest, BadInc) { + LincheckConfiguration conf; + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.iterations(100); + + conf.actorsPerThread(5); + conf.operation("inc"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(CounterTest, BadDec) { + LincheckConfiguration conf; + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.operation("dec"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(CounterTest, BadAdd) { + LincheckConfiguration conf; + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.operation("add"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(CounterTest, GoodDoubleOp) { + LincheckConfiguration conf; + conf.threads(3); + conf.operation("double_op"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(CounterTest, GoodAtomicInc) { + LincheckConfiguration conf; + conf.threads(3); + conf.operation("inc"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(CounterTest, GoodAtomicDec) { + LincheckConfiguration conf; + conf.threads(3); + conf.operation("dec"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(CounterTest, GoodAtomicAdd) { + LincheckConfiguration conf; + conf.threads(3); + conf.operation("add"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(CounterTest, TEST_SHOULD_FAIL_ValidateFunctionsTest) { + LincheckConfiguration conf; + conf.iterations(1); + conf.invocationsPerIteration(1); + conf.threads(2); + conf.validationFunction<&Counter::validate_no_error>(); + conf.validationFunction<&Counter::validate_with_error>(); + conf.operation("add"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(CounterTest, EnabledVerifierTest) { + // This test is needed to compare speed with DisabledVerifierTest + LincheckConfiguration conf; + + conf.iterations(25); + conf.invocationsPerIteration(10); + conf.threads(10); + conf.actorsPerThread(10); + conf.operation("add"); + + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(CounterTest, DisabledVerifierTest) { + // Compare speed with EnabledVerifierTest + LincheckConfiguration conf; + conf.iterations(25); + conf.invocationsPerIteration(10); + // a lot of threads and parallel actors + conf.threads(10); + conf.actorsPerThread(10); + conf.operation("add"); + + conf.disableVerifier(); + ASSERT_EQ(conf.runTest(false), ""); // This should be incredibly fast because of disabled verifier +} \ No newline at end of file diff --git a/cpp/folly_test.cpp b/cpp/folly_test.cpp new file mode 100644 index 000000000..2a9a7146a --- /dev/null +++ b/cpp/folly_test.cpp @@ -0,0 +1,327 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "lincheck.h" +#include "lincheck_functions.h" + +#include "folly/concurrency/ConcurrentHashMap.h" +#include "folly/concurrency/DynamicBoundedQueue.h" +#include "folly/concurrency/UnboundedQueue.h" + +class SequentialMapFolly { +public: + std::unordered_map map; + + SequentialMapFolly() { + map.reserve(100); // to prevent from rehashing and crashes with SIGSEGV, SIGABRT, or new/delete issues + } + + bool assign(int key, int value) { + map.insert_or_assign(key, value); + return true; // concurrent version always returns true + } + + std::pair get(int key) { + auto it = map.find(key); + if (it != map.end()) { + return {true, it->second}; + } + return {false, 0}; + } + + int erase(int key) { + return map.erase(key); + } +}; + +class ConcurrentMapFolly { +public: + folly::ConcurrentHashMap map; + + bool assign(int key, int value) { + return map.insert_or_assign(key, value).second; + } + + std::pair get(int key) { + auto it = map.find(key); + if (it != map.end()) { + return {true, it->second}; + } + return {false, 0}; + } + + int erase(int key) { + return map.erase(key); + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(SequentialMapFolly const &s) const noexcept { + std::vector vec; + for (auto it : s.map) { + vec.push_back(it.first); + vec.push_back(it.second); + } + return Lincheck::hash>()(vec); + } +}; + +bool operator==(const SequentialMapFolly &a, const SequentialMapFolly &b) { + return a.map == b.map; +} + +class SequentialQueueFolly { +public: + std::queue q; + + bool push(int value) { + q.push(value); + return true; + } + + std::pair pop() { + int val = 0; + if(!q.empty()) { + val = q.front(); + q.pop(); + return {true, val}; + } else { + return {false, val}; + } + } +}; + +template +class ConcurrentDynamicBoundedQueueFolly { +public: + DynamicBoundedQueue queue = DynamicBoundedQueue(100); + + bool push(int val) { + return queue.try_enqueue(val); + } + + std::pair pop() { + int ans = 0; + bool success = queue.try_dequeue(ans); + return {success, ans}; + } +}; + +template +class ConcurrentUnboundedQueueFolly { +public: + UnboundedQueue queue; + + bool push(int val) { + queue.enqueue(val); + return true; + } + + std::pair pop() { + int ans = 0; + bool success = queue.try_dequeue(ans); + return {success, ans}; + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(SequentialQueueFolly &s) const noexcept { + std::vector vec; + while(!s.q.empty()) { + vec.push_back(s.q.front()); + s.q.pop(); + } + for (auto it : vec) { + s.q.push(it); + } + return Lincheck::hash>()(vec); + } +}; + +bool operator==(const SequentialQueueFolly &a, const SequentialQueueFolly &b) { + return a.q == b.q; +} + +using namespace Lincheck; + +TEST(FollyTest, BadSequentialMapTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("assign"); + conf.operation, int, &SequentialMapFolly::get, &SequentialMapFolly::get>("get"); + conf.operation("erase"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(FollyTest, BadSequentialQueueTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push"); + conf.operation, &SequentialQueueFolly::pop, &SequentialQueueFolly::pop>("pop"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(FollyTest, HashMapTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("assign"); + conf.operation, int, &ConcurrentMapFolly::get, &SequentialMapFolly::get>("get"); + conf.operation("erase"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, MPMCDynamicBoundedQueueTest) { + using ConcurrentQueue = ConcurrentDynamicBoundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, MPSCDynamicBoundedQueueTest) { + using ConcurrentQueue = ConcurrentDynamicBoundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop", "nonParallelConsumer"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, BadMPSCDynamicBoundedQueueTest) { + using ConcurrentQueue = ConcurrentDynamicBoundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push", "nonParallelProducer"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(FollyTest, SPMCDynamicBoundedQueueTest) { + using ConcurrentQueue = ConcurrentDynamicBoundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push", "nonParallelProducer"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, SPSCDynamicBoundedQueueTest) { + using ConcurrentQueue = ConcurrentDynamicBoundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push", "nonParallelProducer"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop", "nonParallelConsumer"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, BadSPSCDynamicBoundedQueueTest) { + using ConcurrentQueue = ConcurrentDynamicBoundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push", "nonParallelProducer"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(FollyTest, MPMCUnboundedQueueTest) { + using ConcurrentQueue = ConcurrentUnboundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, MPSCUnboundedQueueTest) { + using ConcurrentQueue = ConcurrentUnboundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop", "nonParallelConsumer"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, SPMCUnboundedQueueTest) { + using ConcurrentQueue = ConcurrentUnboundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push", "nonParallelProducer"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop"); + ASSERT_EQ(conf.runTest(false), ""); +} + +TEST(FollyTest, SPSCUnboundedQueueTest) { + using ConcurrentQueue = ConcurrentUnboundedQueueFolly>; + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push", "nonParallelProducer"); + conf.operation, &ConcurrentQueue::pop, &SequentialQueueFolly::pop>("pop", "nonParallelConsumer"); + ASSERT_EQ(conf.runTest(false), ""); +} \ No newline at end of file diff --git a/cpp/libcds_test.cpp b/cpp/libcds_test.cpp new file mode 100644 index 000000000..b91ed0585 --- /dev/null +++ b/cpp/libcds_test.cpp @@ -0,0 +1,311 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "lincheck.h" +#include "lincheck_functions.h" + +class SequentialQueueLibcds { +public: + std::queue q; + + bool push(int value) { + q.push(value); + return true; + } + + std::pair pop() { + int val = 0; + if(!q.empty()) { + val = q.front(); + q.pop(); + return {true, val}; + } else { + return {false, val}; + } + } +}; + +template +class ConcurrentQueueLibcds { +public: + cds::container::MSQueue queue; + + bool push(int val) { + return queue.enqueue(val); + } + + std::pair pop() { + int ans = 0; + bool success = queue.dequeue(ans); + return {success, ans}; + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(SequentialQueueLibcds &s) const noexcept { + std::vector vec; + while(!s.q.empty()) { + vec.push_back(s.q.front()); + s.q.pop(); + } + for (auto it : vec) { + s.q.push(it); + } + return Lincheck::hash>()(vec); + } +}; + +bool operator==(const SequentialQueueLibcds &a, const SequentialQueueLibcds &b) { + return a.q == b.q; +} + +class SequentialStackLibcds { +public: + std::stack s; + + bool push(int value) { + s.push(value); + return true; + } + + std::pair pop() { + int val = 0; + if(!s.empty()) { + val = s.top(); + s.pop(); + return {true, val}; + } else { + return {false, val}; + } + } + + bool empty() { + return s.empty(); + } + + bool clear() { + while(!s.empty()) { + s.pop(); + } + return true; + } +}; + +template +class ConcurrentTreiberStackLibcds { +public: + cds::container::TreiberStack s; + + bool push(int value) { + return s.push(value); + } + + std::pair pop() { + int val = 0; + bool success = s.pop(val); + return {success, success ? val : 0}; + } + + bool empty() { + return s.empty(); + } + + bool clear() { + s.clear(); + return true; + } +}; + +class ConcurrentFCStackLibcds { +public: + cds::container::FCStack s; + + bool push(int value) { + return s.push(value); + } + + std::pair pop() { + int val = 0; + bool success = s.pop(val); + return {success, success ? val : 0}; + } + + bool empty() { + return s.empty(); + } + + bool clear() { + s.clear(); + return true; + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(SequentialStackLibcds &s) const noexcept { + std::vector vec; + while(!s.s.empty()) { + vec.push_back(s.s.top()); + s.s.pop(); + } + reverse(vec.begin(), vec.end()); + for (auto it : vec) { + s.s.push(it); + } + return Lincheck::hash>()(vec); + } +}; + +bool operator==(const SequentialStackLibcds &a, const SequentialStackLibcds &b) { + return a.s == b.s; +} + +void myAttach() { + //std::string val = "attached " + std::to_string(cds::OS::get_current_thread_id()) + "\n"; + //std::cout << val; + cds::threading::Manager::attachThread(); +} + +void myDetach() { + //std::string val = "detached " + std::to_string(cds::OS::get_current_thread_id()) + "\n"; + //std::cout << val; + cds::threading::Manager::detachThread(); +} + +using namespace Lincheck; + + +TEST(LibcdsTest, BadSequentialQueueTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push"); + conf.operation, &SequentialQueueLibcds::pop, &SequentialQueueLibcds::pop>("pop"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(LibcdsTest, BadSequentialStackTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("push"); + conf.operation, &SequentialStackLibcds::pop, &SequentialStackLibcds::pop>("pop"); + conf.operation("clear"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(LibcdsTest, ConcurrentQueueHPTest) { + cds::Initialize(); + { + cds::gc::HP dhpGC( + 16 + ); + LincheckConfiguration, SequentialQueueLibcds> conf; + conf.iterations(100); + conf.invocationsPerIteration(500); + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + conf.initThreadFunction(); + conf.finishThreadFunction(); + conf.operation::push, &SequentialQueueLibcds::push>("push"); + conf.operation, &ConcurrentQueueLibcds::pop, &SequentialQueueLibcds::pop>("pop"); + ASSERT_EQ(conf.runTest(false), ""); + } + cds::Terminate(); +} + +TEST(LibcdsTest, ConcurrentQueueDHPTest) { + cds::Initialize(); + { + cds::gc::DHP dhpGC( + 160 //dhp_init_guard_count + ); + LincheckConfiguration, SequentialQueueLibcds> conf; + conf.iterations(100); + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + conf.initThreadFunction(); + conf.finishThreadFunction(); + conf.operation::push, &SequentialQueueLibcds::push>("push"); + conf.operation, &ConcurrentQueueLibcds::pop, &SequentialQueueLibcds::pop>("pop"); + ASSERT_EQ(conf.runTest(false), ""); + } + cds::Terminate(); +} + +TEST(LibcdsTest, ConcurrentTreiberStackHPTest) { + cds::Initialize(); + { + cds::gc::HP dhpGC( + 16 + ); + LincheckConfiguration, SequentialStackLibcds> conf; + conf.iterations(100); + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + conf.initThreadFunction(); + conf.finishThreadFunction(); + conf.operation::push, &SequentialStackLibcds::push>("push"); + conf.operation, &ConcurrentTreiberStackLibcds::pop, &SequentialStackLibcds::pop>("pop"); + conf.operation::clear, &SequentialStackLibcds::clear>("clear"); + ASSERT_EQ(conf.runTest(false), ""); + } + cds::Terminate(); +} + +TEST(LibcdsTest, ConcurrentTreiberStackDHPTest) { + cds::Initialize(); + { + cds::gc::DHP dhpGC( + 160 + ); + LincheckConfiguration, SequentialStackLibcds> conf; + conf.iterations(100); + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + conf.initThreadFunction(); + conf.finishThreadFunction(); + conf.operation::push, &SequentialStackLibcds::push>("push"); + conf.operation, &ConcurrentTreiberStackLibcds::pop, &SequentialStackLibcds::pop>("pop"); + conf.operation::clear, &SequentialStackLibcds::clear>("clear"); + ASSERT_EQ(conf.runTest(false), ""); + } + cds::Terminate(); +} + +/* +TEST(LibcdsTest, ConcurrentFCStackTest) { + cds::Initialize(); + { + LincheckConfiguration conf; + conf.iterations(1); + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + conf.operation("push"); + conf.operation, &ConcurrentFCStackLibcds::pop, &SequentialStackLibcds::pop>("pop"); + conf.operation("clear"); + ASSERT_EQ(conf.runTest(false), ""); + } + cds::Terminate(); +} +*/ \ No newline at end of file diff --git a/cpp/libcuckoo_test.cpp b/cpp/libcuckoo_test.cpp new file mode 100644 index 000000000..ca00369e7 --- /dev/null +++ b/cpp/libcuckoo_test.cpp @@ -0,0 +1,123 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "lincheck.h" +#include "lincheck_functions.h" + +#include "libcuckoo/cuckoohash_map.hh" + +class SequentialMapCuckoo { +public: + std::unordered_map map; + + SequentialMapCuckoo() { + map.reserve(100); + } + + bool assign(int key, int value) { + //std::cerr << "seqassign " << key << ", " << value << "\n"; + auto it = map.find(key); + if (it != map.end()) { + map[key] = value; + return false; + } + map[key] = value; + return true; + } + + int get(int key) { + //std::cerr << "seqget " << key << "\n"; + auto it = map.find(key); + if (it != map.end()) { + return it->second; + } + return -239; + } + + bool erase(int key) { + //std::cerr << "seqerase " << key << "\n"; + auto it = map.find(key); + if (it != map.end()) { + map.erase(key); + return true; + } + map.erase(key); + return false; + } +}; + +class ConcurrentMapCuckoo { +public: + libcuckoo::cuckoohash_map map; + + bool assign(int key, int value) { + //std::cerr << "assign " << key << ", " << value << "\n"; + auto ans = map.insert_or_assign(key, value); + //std::cerr << "assign2 " << key << ", " << value << "\n"; + return ans; + } + + int get(int key) { + //std::cerr << "get " << key << "\n"; + try { + auto ans = map.find(key); + //std::cerr << "get2 " << key << "\n"; + return ans; + } catch (std::out_of_range &e) { + //std::cerr << "get3 " << key << "\n"; + return -239; + } + } + + bool erase(int key) { + //std::cerr << "erase " << key << "\n"; + auto ans = map.erase(key); + //std::cerr << "erase2 " << key << "\n"; + return ans; + } +}; + +template<> +struct Lincheck::hash { + std::size_t operator()(SequentialMapCuckoo const &s) const noexcept { + std::vector vec; + for (auto it : s.map) { + vec.push_back(it.first); + vec.push_back(it.second); + } + return Lincheck::hash>()(vec); + } +}; + +bool operator==(const SequentialMapCuckoo &a, const SequentialMapCuckoo &b) { + return a.map == b.map; +} + +using namespace Lincheck; + +TEST(LibcuckooTest, BadSequentialMapTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("assign"); + conf.operation("get"); + conf.operation("erase"); + ASSERT_THAT(conf.runTest(false), ::testing::HasSubstr("Invalid execution results")); +} + +TEST(LibcuckooTest, HashMapTest) { + LincheckConfiguration conf; + conf.iterations(100); + + conf.minimizeFailedScenario(false); + conf.threads(3); + conf.actorsPerThread(4); + + conf.operation("assign"); + conf.operation("get"); + conf.operation("erase"); + ASSERT_EQ(conf.runTest(false), ""); +} \ No newline at end of file diff --git a/cpp/lincheck.h b/cpp/lincheck.h new file mode 100644 index 000000000..6ffedc8ab --- /dev/null +++ b/cpp/lincheck.h @@ -0,0 +1,309 @@ +#pragma once + +#include +#include +#include +#include "../build/bin/native/releaseShared/libnative_api.h" + +extern libnative_ExportedSymbols *libnative_symbols(void); + +namespace Lincheck { + template + struct to_string { + std::string operator()(const type &val) const noexcept { + return std::to_string(val); + } + }; + + template + struct hash { + std::size_t operator()(const type &val) const noexcept { + return std::hash()(val); + } + }; + + template + class ParameterGenerator { + std::mt19937 rnd = std::mt19937(rand()); + public: + type generate() { + return rnd() % 16 - 5; + } + }; + + template + class LincheckConfiguration { + libnative_ExportedSymbols *lib = libnative_symbols(); + libnative_kref_org_jetbrains_kotlinx_lincheck_NativeAPIStressConfiguration configuration = lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.NativeAPIStressConfiguration(); + + typedef void *(*constructor_pointer)(); + + typedef void (*destructor_pointer)(void *); + + typedef bool (*equals_pointer)(void *, void *); + + typedef int (*hashCode_pointer)(void *); + + public: + LincheckConfiguration() { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupInitialState( + configuration, + (void *) (constructor_pointer) []() -> void * { return new TestClass(); }, // constructor + (void *) (destructor_pointer) [](void *p) { delete (TestClass *) p; } // destructor + ); + + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupSequentialSpecification( + configuration, + (void *) (constructor_pointer) []() -> void * { return new SequentialSpecification(); }, // constructor + (void *) (destructor_pointer) [](void *p) { delete (SequentialSpecification *) p; }, // destructor + (void *) (equals_pointer) [](void *a, void *b) -> bool { + return *(SequentialSpecification *) a == *(SequentialSpecification *) b; + }, // equals + (void *) (hashCode_pointer) [](void *instance) -> int { + return Lincheck::hash()(*(SequentialSpecification *) instance); + } // hashCode + ); + } + + void iterations(int count) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupIterations( + configuration, count); + } + + void invocationsPerIteration(int count) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupInvocationsPerIteration( + configuration, count); + } + + void minimizeFailedScenario(bool minimizeFailedScenario) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupMinimizeFailedScenario( + configuration, minimizeFailedScenario); + } + + void threads(int count) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupThreads( + configuration, count); + } + + void actorsPerThread(int count) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupActorsPerThread( + configuration, count); + } + + void actorsBefore(int count) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupActorsBefore( + configuration, count); + } + + void actorsAfter(int count) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupActorsAfter( + configuration, count); + } + + template + void initThreadFunction() { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupInitThreadFunction( + configuration, (void *) (void (*)()) []() { f(); }); + } + + template + void finishThreadFunction() { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupFinishThreadFunction( + configuration, (void *) (void (*)()) []() { f(); }); + } + + /* + * Change verifier to EpsilonVerifier + */ + void disableVerifier() { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.disableVerifier(configuration); + } + + template + void validationFunction() { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupValidationFunction( + configuration, + (void *) (void (*)(void *)) [](void *instance) -> void { // validate + auto *obj = (TestClass *) instance; // add type to void* + try { + (obj->*validate)(); // invoke op method + } catch(const std::exception& e) { + std::string message = "Validation error: \"" + std::string(e.what()) + "\""; + libnative_symbols()->kotlin.root.org.jetbrains.kotlinx.lincheck.throwKotlinValidationException(message.c_str()); + } catch(...) { + std::cerr << "Validate function should throw std::exception, but something different was thrown\n"; + } + } + ); + } + + template + void operation(const char *operationName, const char *nonParallelGroupName = nullptr, bool useOnce = false) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupOperation1( + configuration, + (void *) (void *(*)(void *)) [](void *instance) -> void * { // operation + auto *obj = (TestClass *) instance; // add type to void* + Ret res = (obj->*op)(); // invoke op method + return new Ret(res); // copy from stack to heap and return + }, + (void *) (void *(*)(void *)) [](void *instance) -> void * { // sequential specification + auto *obj = (SequentialSpecification *) instance; // add type to void* + Ret res = (obj->*seq_spec)(); // invoke op method + return new Ret(res); // copy from stack to heap and return + }, + (void *) (void (*)(void *)) [](void *ret) { // Ret destructor + Ret *obj = (Ret *) ret; // add type to void* + delete obj; // destructor + }, + (void *) (bool (*)(void *, void *)) [](void *a, void *b) -> bool { // Ret equals + Ret *obj_a = (Ret *) a; // add type to void* + Ret *obj_b = (Ret *) b; // add type to void* + return *obj_a == *obj_b; + }, + (void *) (int (*)(void *)) [](void *ret) -> int { // Ret hashCode + return Lincheck::hash()(*(Ret *) ret); + }, + (void *) (void (*)(void *, char *, int)) [](void *ret, char *dest, int destSize) { // Ret toString + strncpy(dest, Lincheck::to_string()(*(Ret *) ret).c_str(), destSize); + }, + operationName, + nonParallelGroupName, + useOnce + ); + } + + template + void operation(const char *operationName, const char *nonParallelGroupName = nullptr, bool useOnce = false) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupOperation2( + configuration, + (void *) (void *(*)()) []() -> void * { // arg1_gen_initial_state + return new ParameterGenerator(); + }, + (void *) (void *(*)(void *)) [](void *gen) -> void * { // arg1_gen_generate + auto *obj = (ParameterGenerator *) gen; // add type to void* + Arg1 arg = obj->generate(); // invoke generate method + return new Arg1(arg); // copy from stack to heap and return + }, + (void *) (void (*)(void *, char *, int)) [](void *arg, char *dest, int destSize) { // arg1_toString + strncpy(dest, Lincheck::to_string()(*(Arg1 *) arg).c_str(), destSize); + }, + (void *) (void (*)(void *)) [](void *arg1) { // arg1_destructor + Arg1 *obj = (Arg1 *) arg1; // add type to void* + delete obj; // destructor + }, + (void *) (void *(*)(void *, void *)) [](void *instance, void *arg1) -> void * { // operation + auto *obj = (TestClass *) instance; // add type to void* + auto *a1 = (Arg1 *) arg1; + Ret res = (obj->*op)(*a1); // invoke op method + return new Ret(res); // copy from stack to heap and return + }, + (void *) (void *(*)(void *, void *)) [](void *instance, + void *arg1) -> void * { // sequential specification + auto *obj = (SequentialSpecification *) instance; // add type to void* + auto *a1 = (Arg1 *) arg1; + Ret res = (obj->*seq_spec)(*a1); // invoke op method + return new Ret(res); // copy from stack to heap and return + }, + (void *) (void (*)(void *)) [](void *ret) { // Ret destructor + Ret *obj = (Ret *) ret; // add type to void* + delete obj; // destructor + }, + (void *) (bool (*)(void *, void *)) [](void *a, void *b) -> bool { // Ret equals + Ret *obj_a = (Ret *) a; // add type to void* + Ret *obj_b = (Ret *) b; // add type to void* + return *obj_a == *obj_b; + }, + (void *) (int (*)(void *)) [](void *ret) -> int { // Ret hashCode + return Lincheck::hash()(*(Ret *) ret); + }, + (void *) (void (*)(void *, char *, int)) [](void *ret, char *dest, int destSize) { // Ret toString + strncpy(dest, Lincheck::to_string()(*(Ret *) ret).c_str(), destSize); + }, + operationName, + nonParallelGroupName, + useOnce + ); + } + + + template + void operation(const char *operationName, const char *nonParallelGroupName = nullptr, bool useOnce = false) { + lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.setupOperation3( + configuration, + (void *) (void *(*)()) []() -> void * { // arg1_gen_initial_state + return new ParameterGenerator(); + }, + (void *) (void *(*)(void *)) [](void *gen) -> void * { // arg1_gen_generate + auto *obj = (ParameterGenerator *) gen; // add type to void* + Arg1 arg = obj->generate(); // invoke generate method + return new Arg1(arg); // copy from stack to heap and return + }, + (void *) (void (*)(void *, char *, int)) [](void *arg, char *dest, int destSize) { // arg1_toString + strncpy(dest, Lincheck::to_string()(*(Arg1 *) arg).c_str(), destSize); + }, + (void *) (void (*)(void *)) [](void *arg1) { // arg1_destructor + Arg1 *obj = (Arg1 *) arg1; // add type to void* + delete obj; // destructor + }, + (void *) (void *(*)()) []() -> void * { // arg2_gen_initial_state + return new ParameterGenerator(); + }, + (void *) (void *(*)(void *)) [](void *gen) -> void * { // arg2_gen_generate + auto *obj = (ParameterGenerator *) gen; // add type to void* + Arg2 arg = obj->generate(); // invoke generate method + return new Arg2(arg); // copy from stack to heap and return + }, + (void *) (void (*)(void *, char *, int)) [](void *arg, char *dest, int destSize) { // arg2_toString + strncpy(dest, Lincheck::to_string()(*(Arg2 *) arg).c_str(), destSize); + }, + (void *) (void (*)(void *)) [](void *arg2) { // arg2_destructor + Arg2 *obj = (Arg2 *) arg2; // add type to void* + delete obj; // destructor + }, + (void *) (void *(*)(void *, void *, void *)) [](void *instance, void *arg1, + void *arg2) -> void * { // operation + auto *obj = (TestClass *) instance; // add type to void* + auto *a1 = (Arg1 *) arg1; + auto *a2 = (Arg2 *) arg2; + Ret res = (obj->*op)(*a1, *a2); // invoke op method + return new Ret(res); // copy from stack to heap and return + }, + (void *) (void *(*)(void *, void *, void *)) [](void *instance, + void *arg1, + void *arg2) -> void * { // sequential specification + auto *obj = (SequentialSpecification *) instance; // add type to void* + auto *a1 = (Arg1 *) arg1; + auto *a2 = (Arg2 *) arg2; + Ret res = (obj->*seq_spec)(*a1, *a2); // invoke op method + return new Ret(res); // copy from stack to heap and return + }, + (void *) (void (*)(void *)) [](void *ret) { // Ret destructor + Ret *obj = (Ret *) ret; // add type to void* + delete obj; // destructor + }, + (void *) (bool (*)(void *, void *)) [](void *a, void *b) -> bool { // Ret equals + Ret *obj_a = (Ret *) a; // add type to void* + Ret *obj_b = (Ret *) b; // add type to void* + return *obj_a == *obj_b; + }, + (void *) (int (*)(void *)) [](void *ret) -> int { // Ret hashCode + return Lincheck::hash()(*(Ret *) ret); + }, + (void *) (void (*)(void *, char *, int)) [](void *ret, char *dest, int destSize) { // Ret toString + strncpy(dest, Lincheck::to_string()(*(Ret *) ret).c_str(), destSize); + }, + operationName, + nonParallelGroupName, + useOnce + ); + } + + std::string runTest(bool printErrorToStderr = true) { + return lib->kotlin.root.org.jetbrains.kotlinx.lincheck.NativeAPIStressConfiguration.runNativeTest( + configuration, printErrorToStderr); + } + }; +} diff --git a/cpp/lincheck_functions.h b/cpp/lincheck_functions.h new file mode 100644 index 000000000..daeac9e60 --- /dev/null +++ b/cpp/lincheck_functions.h @@ -0,0 +1,25 @@ +#include "lincheck.h" +template<> +struct Lincheck::hash> { + std::size_t operator()(const std::vector &v) const noexcept { + std::string s; + for(auto elem : v) { + s += std::to_string(elem) + ","; + } + return std::hash()(s); + } +}; + +template<> +struct Lincheck::to_string> { + std::string operator()(const std::pair &ret) const noexcept { + return ret.first ? "{true, " + std::to_string(ret.second) + "}" : "{false, " + std::to_string(ret.second) + "}"; + } +}; + +template<> +struct Lincheck::hash> { + std::size_t operator()(std::pair &p) const noexcept { + return p.first ? Lincheck::hash()(p.second) : -1 * Lincheck::hash()(p.second); + } +}; \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index a6aab3ebc..d6bf6c4e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,15 +5,20 @@ version=2.15-SNAPSHOT inceptionYear=2019 lastCopyrightYear=2020 -kotlinVersion=1.4.0 -kotlinxCoroutinesVersion=1.4.0 -asmVersion=9.0 -atomicfuVersion=0.14.4 +kotlinVersion=1.4.32 +kotlinxCoroutinesVersion=1.4.3 +asmVersion=9.1 +atomicfuVersion=0.15.2 reflectionsVersion=0.9.12 junitVersion=4.13.1 jctoolsVersion=2.1.0 +native.deploy= + +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.mpp.enableCompatibilityMetadataVariant=true + # Workaround for Bintray treating .sha512 files as artifacts # https://github.com/gradle/gradle/issues/11412 systemProp.org.gradle.internal.publish.checksums.insecure=true \ No newline at end of file diff --git a/gradle/native-targets.gradle b/gradle/native-targets.gradle new file mode 100644 index 000000000..04e5349ba --- /dev/null +++ b/gradle/native-targets.gradle @@ -0,0 +1,172 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +/** + * Specifies what subset of Native targets to build + * + * ALL — all possible for compilation + * HOST — host-specific ones, without cross compilation (e.g. do not build linuxX64 on macOS host) + * SINGLE — only for current OS (to import into IDEA / quickly run tests) + * DISABLED — disable all Native targets (useful with Kotlin compiler built from sources) + * + * For HOST mode, all targets are still listed in .module file, so HOST mode is useful for release process. + */ +enum NativeState { ALL, HOST, SINGLE, DISABLED } + +def getNativeState(String description) { + if (description == null) return NativeState.SINGLE + switch(description.toLowerCase()) { + case 'all': + case 'true': + return NativeState.ALL + case 'host': + return NativeState.HOST + case 'disabled': + return NativeState.DISABLED + // 'single', 'false', etc + default: + return NativeState.SINGLE + } +} + +project.ext.ideaActive = System.getProperty('idea.active') == 'true' +project.ext.nativeState = getNativeState(property('native.deploy')) +project.ext.singleTargetMode = project.ext.ideaActive || (project.ext.nativeState == NativeState.SINGLE) + +project.ext.nativeMainSets = [] +project.ext.nativeTestSets = [] + +/** + * Disables compilation but leaves the target in .module file + */ +def disableCompilation(targets) { + configure(targets) { + compilations.all { + cinterops.all { project.tasks[interopProcessingTaskName].enabled = false } + compileKotlinTask.enabled = false + } + binaries.all { linkTask.enabled = false } + + mavenPublication { publicationToDisable -> + tasks.withType(AbstractPublishToMaven).all { + onlyIf { publication != publicationToDisable } + } + tasks.withType(GenerateModuleMetadata).all { + onlyIf { publication.get() != publicationToDisable } + } + } + } +} + +def getHostName() { + def target = System.getProperty("os.name") + if (target == 'Linux') return 'linux' + if (target.startsWith('Windows')) return 'windows' + if (target.startsWith('Mac')) return 'macos' + return 'unknown' +} + +kotlin { + targets { + def manager = project.ext.hostManager + def linuxEnabled = manager.isEnabled(presets.linuxX64.konanTarget) + def macosEnabled = manager.isEnabled(presets.macosX64.konanTarget) + def winEnabled = manager.isEnabled(presets.mingwX64.konanTarget) + + def ideaPreset = presets.linuxX64 + if (macosEnabled) ideaPreset = presets.macosX64 + if (winEnabled) ideaPreset = presets.mingwX64 + project.ext.ideaPreset = ideaPreset + } + + targets.metaClass.addTarget = { preset -> + def target = delegate.fromPreset(preset, preset.name) + project.ext.nativeMainSets.add(target.compilations['main'].kotlinSourceSets.first()) + project.ext.nativeTestSets.add(target.compilations['test'].kotlinSourceSets.first()) + } + + targets { + if (project.ext.nativeState == NativeState.DISABLED) return + if (project.ext.singleTargetMode) { + fromPreset(project.ext.ideaPreset, 'native') { + binaries { + sharedLib { + baseName = "native" // on Linux and macOS + // baseName = "libnative" //on Windows + } + } + } + } else { + // Linux + addTarget(presets.linuxX64) + addTarget(presets.linuxArm32Hfp) + addTarget(presets.linuxArm64) + + // Mac & iOS + addTarget(presets.macosX64) + + addTarget(presets.iosArm64) + addTarget(presets.iosArm32) + addTarget(presets.iosX64) + + addTarget(presets.watchosX86) + addTarget(presets.watchosArm32) + addTarget(presets.watchosArm64) + + addTarget(presets.tvosArm64) + addTarget(presets.tvosX64) + + // Windows + addTarget(presets.mingwX64) + addTarget(presets.mingwX86) + } + + if (project.ext.nativeState == NativeState.HOST) { + // linux targets that can cross-compile on all three hosts + def linuxCrossCompileTargets = [linuxX64, linuxArm32Hfp, linuxArm64] + if (getHostName() != "linux") { + disableCompilation(linuxCrossCompileTargets) + } + } + } + + + sourceSets { + nativeMain { + dependsOn commonMain + kotlin.srcDirs = ['src/native/main'] + } + // Empty source set is required in order to have native tests task + nativeTest { + dependsOn commonTest + kotlin.srcDirs = ['src/native/test'] + } + + if (!project.ext.singleTargetMode) { + configure(project.ext.nativeMainSets) { + dependsOn nativeMain + } + + configure(project.ext.nativeTestSets) { + dependsOn nativeTest + } + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 12d38de6a..8cf6eb5ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..ac1b06f93 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/Actor.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/Actor.kt new file mode 100644 index 000000000..637347043 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/Actor.kt @@ -0,0 +1,44 @@ +/* + * 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.verifier.quiescent.* +import kotlin.reflect.KClass + +/** + * The actor entity describe the operation with its parameters + * which is executed during the testing. + * + * @see Operation + */ +expect class Actor { + val isSuspendable: Boolean + val allowExtraSuspension: Boolean + val promptCancellation: Boolean + val cancelOnSuspension: Boolean + val handledExceptions: List> + + override fun toString(): String + + fun finalize() +} + +expect val Actor.isQuiescentConsistent: Boolean \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt new file mode 100644 index 000000000..bfb51044c --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt @@ -0,0 +1,39 @@ +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* +import kotlin.reflect.* + +/** + * Abstract configuration for different lincheck modes. + */ +abstract class CTestConfiguration( + val testClass: TestClass, + val iterations: Int, + val threads: Int, + val actorsPerThread: Int, + val actorsBefore: Int, + val actorsAfter: Int, + val executionGenerator: (testConfiguration: CTestConfiguration, testStructure: CTestStructure) -> ExecutionGenerator, + val verifierGenerator: (sequentialSpecification: SequentialSpecification<*>) -> Verifier, + val requireStateEquivalenceImplCheck: Boolean, + val minimizeFailedScenario: Boolean, + val sequentialSpecification: SequentialSpecification<*>, + val timeoutMs: Long, + val customScenarios: List +) { + abstract fun createStrategy(testClass: TestClass, scenario: ExecutionScenario, validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, verifier: Verifier): Strategy + + companion object { + const val DEFAULT_ITERATIONS = 100 + const val DEFAULT_THREADS = 2 + const val DEFAULT_ACTORS_PER_THREAD = 5 + const val DEFAULT_ACTORS_BEFORE = 5 + const val DEFAULT_ACTORS_AFTER = 5 + const val DEFAULT_MINIMIZE_ERROR = true + const val DEFAULT_TIMEOUT_MS: Long = 10000 + } +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/CTestStructure.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/CTestStructure.kt new file mode 100644 index 000000000..5e46f97d3 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/CTestStructure.kt @@ -0,0 +1,56 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.execution.* + +expect class ValidationFunction + +expect val ValidationFunction.name: String + +expect class StateRepresentationFunction + +/** + * Contains information about the provided operations (see [Operation]). + * Several [tests][StressCTest] can refer to one structure + * (i.e. one test class could have several [StressCTest] annotations) + */ +expect class CTestStructure { + val actorGenerators: List + val operationGroups: List + val validationFunctions: List + val stateRepresentation: StateRepresentationFunction? +} + +class OperationGroup(val name: String, val nonParallel: Boolean) { + val actors: MutableList + override fun toString(): String { + return "OperationGroup{" + + "name='" + name + '\'' + + ", nonParallel=" + nonParallel + + ", actors=" + actors + + '}' + } + + init { + actors = ArrayList() + } +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/CommonCustomScenarioDSL.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/CommonCustomScenarioDSL.kt new file mode 100644 index 000000000..e2c0820a4 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/CommonCustomScenarioDSL.kt @@ -0,0 +1,50 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.execution.* + +expect class ScenarioBuilder() { + fun buildScenario(): ExecutionScenario +} + +/** + * Creates a custom scenario using the specified DSL as in the following example: + * + * ``` + * scenario { + * initial { + * actor(QueueTest::offer, 1) + * actor(QueueTest::offer, 2) + * } + * parallel { + * thread { + * actor(QueueTest::poll) + * } + * thread { + * actor(QueueTest::poll) + * } + * } + * } + * ``` + */ +fun scenario(block: ScenarioBuilder.() -> Unit): ExecutionScenario = + ScenarioBuilder().apply(block).buildScenario() diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/CommonReporter.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/CommonReporter.kt new file mode 100644 index 000000000..671b58b8c --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/CommonReporter.kt @@ -0,0 +1,177 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.LoggingLevel.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import kotlinx.atomicfu.locks.* +import kotlin.jvm.* + +class Reporter @JvmOverloads constructor(val logLevel: LoggingLevel) : SynchronizedObject() { + + fun logIteration(iteration: Int, maxIterations: Int, scenario: ExecutionScenario) = log(INFO) { + appendLine("\n= Iteration $iteration / $maxIterations =") + appendExecutionScenario(scenario) + } + + fun logFailedIteration(failure: LincheckFailure) = log(INFO) { + appendFailure(failure) + } + + fun logScenarioMinimization(scenario: ExecutionScenario) = log(INFO) { + appendLine("\nInvalid interleaving found, trying to minimize the scenario below:") + appendExecutionScenario(scenario) + } + + fun logStateEquivalenceViolation(sequentialSpecification: SequentialSpecification<*>) = log(WARN) { + appendStateEquivalenceViolationMessage(sequentialSpecification) + } + + private inline fun log(logLevel: LoggingLevel, crossinline msg: StringBuilder.() -> Unit): Unit = synchronized(this) { + if (this.logLevel > logLevel) return + val sb = StringBuilder() + msg(sb) + if (logLevel == WARN) { + printErr(sb) + } else { + println(sb) + } + } +} + +fun printErr(sb: StringBuilder) { + printErr(sb.toString()) +} + +expect fun printErr(message: String) + +@JvmField val DEFAULT_LOG_LEVEL = WARN +enum class LoggingLevel { + INFO, WARN +} + +private class ActorWithResult(val actorRepresentation: String, val spacesAfterActor: Int, + val resultRepresentation: String, val spacesAfterResult: Int, + val clockRepresentation: String) { + override fun toString(): String = + actorRepresentation + ":" + " ".repeat(spacesAfterActor) + resultRepresentation + + " ".repeat(spacesAfterResult) + clockRepresentation +} + +private fun uniteActorsAndResultsLinear(actors: List, results: List): List { + require(actors.size == results.size) { + "Different numbers of actors and matching results found (${actors.size} != ${results.size})" + } + return actors.indices.map { + ActorWithResult("${actors[it]}", 1, "${results[it]}", 0, "") + } +} + +private fun uniteParallelActorsAndResults(actors: List>, results: List>): List> { + require(actors.size == results.size) { + "Different numbers of threads and matching results found (${actors.size} != ${results.size})" + } + return actors.mapIndexed { id, threadActors -> uniteActorsAndResultsAligned(threadActors, results[id]) } +} + +private fun uniteActorsAndResultsAligned(actors: List, results: List): List { + require(actors.size == results.size) { + "Different numbers of actors and matching results found (${actors.size} != ${results.size})" + } + val actorRepresentations = actors.map { it.toString() } + val resultRepresentations = results.map { it.result.toString() } + val maxActorLength = actorRepresentations.map { it.length }.max()!! + val maxResultLength = resultRepresentations.map { it.length }.max()!! + return actors.indices.map { i -> + val actorRepr = actorRepresentations[i] + val resultRepr = resultRepresentations[i] + val clock = results[i].clockOnStart + val spacesAfterActor = maxActorLength - actorRepr.length + 1 + val spacesAfterResultToAlign = maxResultLength - resultRepr.length + if (clock.empty) { + ActorWithResult(actorRepr, spacesAfterActor, resultRepr, spacesAfterResultToAlign, "") + } else { + ActorWithResult(actorRepr, spacesAfterActor, resultRepr, spacesAfterResultToAlign + 1, clock.toString()) + } + } +} + +internal expect fun StringBuilder.appendFailure(failure: LincheckFailure): StringBuilder + +internal fun StringBuilder.appendUnexpectedExceptionFailure(failure: UnexpectedExceptionFailure): StringBuilder { + appendLine("= The execution failed with an unexpected exception =") + appendExecutionScenario(failure.scenario) + appendLine() + appendException(failure.exception) + return this +} + +internal expect fun StringBuilder.appendDeadlockWithDumpFailure(failure: DeadlockWithDumpFailure): StringBuilder + +internal fun StringBuilder.appendIncorrectResultsFailure(failure: IncorrectResultsFailure): StringBuilder { + appendLine("= Invalid execution results =") + if (failure.scenario.initExecution.isNotEmpty()) { + appendLine("Init part:") + appendLine(uniteActorsAndResultsLinear(failure.scenario.initExecution, failure.results.initResults)) + } + if (failure.results.afterInitStateRepresentation != null) + appendLine("STATE: ${failure.results.afterInitStateRepresentation}") + appendLine("Parallel part:") + val parallelExecutionData = uniteParallelActorsAndResults(failure.scenario.parallelExecution, failure.results.parallelResultsWithClock) + append(printInColumns(parallelExecutionData)) + if (failure.results.afterParallelStateRepresentation != null) { + appendLine() + append("STATE: ${failure.results.afterParallelStateRepresentation}") + } + if (failure.scenario.postExecution.isNotEmpty()) { + appendLine() + appendLine("Post part:") + append(uniteActorsAndResultsLinear(failure.scenario.postExecution, failure.results.postResults)) + } + if (failure.results.afterPostStateRepresentation != null && failure.scenario.postExecution.isNotEmpty()) { + appendLine() + append("STATE: ${failure.results.afterPostStateRepresentation}") + } + if (failure.results.parallelResultsWithClock.flatten().any { !it.clockOnStart.empty }) + appendLine("\n---\nvalues in \"[..]\" brackets indicate the number of completed operations \n" + + "in each of the parallel threads seen at the beginning of the current operation\n---") + return this +} + +internal fun StringBuilder.appendValidationFailure(failure: ValidationFailure): StringBuilder { + appendLine("= Validation function ${failure.functionName} has failed =") + appendExecutionScenario(failure.scenario) + appendException(failure.exception) + return this +} + +internal fun StringBuilder.appendObstructionFreedomViolationFailure(failure: ObstructionFreedomViolationFailure): StringBuilder { + appendLine("= ${failure.reason} =") + appendExecutionScenario(failure.scenario) + return this +} + +private fun StringBuilder.appendException(t: Throwable) { + appendLine(t.stackTraceToString()) +} + +internal expect fun StringBuilder.appendStateEquivalenceViolationMessage(sequentialSpecification: SequentialSpecification<*>) \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/CommonUtils.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/CommonUtils.kt new file mode 100644 index 000000000..ea4609487 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/CommonUtils.kt @@ -0,0 +1,183 @@ +/*- + * #%L + * Lincheck + * %% + * Copyright (C) 2019 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 + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.runner.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import kotlin.coroutines.* +import kotlin.jvm.* +import kotlin.reflect.* + +expect class TestClass { + fun createInstance(): Any +} + +expect class SequentialSpecification + +expect fun SequentialSpecification.getInitialState(): T + +expect fun chooseSequentialSpecification(sequentialSpecificationByUser: SequentialSpecification<*>?, testClass: TestClass): SequentialSpecification<*> + +//@Volatile +internal var storedLastCancellableCont: CancellableContinuation<*>? = null + +expect fun storeCancellableContinuation(cont: CancellableContinuation<*>) + +internal fun executeActor(testInstance: Any, actor: Actor) = executeActor(testInstance, actor, null) +internal expect fun executeActor( + instance: Any, + actor: Actor, + completion: Continuation? +): Result + +internal expect fun createLincheckResult(res: Any?, wasSuspended: Boolean = false): Result + +expect fun loadSequentialSpecification(sequentialSpecification: SequentialSpecification<*>): SequentialSpecification + +internal expect fun executeValidationFunction(instance: Any, validationFunction: ValidationFunction): Throwable? +internal inline fun executeValidationFunctions(instance: Any, validationFunctions: List, + onError: (functionName: String, exception: Throwable) -> Unit) { + for (f in validationFunctions) { + val validationException = executeValidationFunction(instance, f) + if (validationException != null) { + onError(f.name, validationException) + return + } + } +} + +/** + * loader class type should be ClassLoader in jvm + */ +internal expect fun ExecutionScenario.convertForLoader(loader: Any): ExecutionScenario + +/** + * Returns `true` if the continuation was cancelled by [CancellableContinuation.cancel]. + */ +fun kotlin.Result.cancelledByLincheck() = exceptionOrNull() === cancellationByLincheckException + +private val cancellationByLincheckException = Exception("Cancelled by lincheck") + +internal enum class CancellationResult { CANCELLED_BEFORE_RESUMPTION, CANCELLED_AFTER_RESUMPTION, CANCELLATION_FAILED } + +internal class StoreExceptionHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { + var exception: Throwable? = null + + override fun handleException(context: CoroutineContext, exception: Throwable) { + this.exception = exception + } +} + +class LincheckAssertionError( + failure: LincheckFailure +) : AssertionError("\n" + failure) + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +internal fun CancellableContinuation.cancelByLincheck(promptCancellation: Boolean): CancellationResult { + val exceptionHandler = context[CoroutineExceptionHandler] as StoreExceptionHandler + exceptionHandler.exception = null + val cancelled = cancel(cancellationByLincheckException) + exceptionHandler.exception?.let { + throw it.cause!! // let's throw the original exception, ignoring the internal coroutines details + } + return when { + cancelled -> CancellationResult.CANCELLED_BEFORE_RESUMPTION + promptCancellation -> { + context[Job]!!.cancel() // we should always put a job into the context for prompt cancellation + CancellationResult.CANCELLED_AFTER_RESUMPTION + } + else -> CancellationResult.CANCELLATION_FAILED + } +} + +/** + * Returns scenario for the specified thread. Note that initial and post parts + * are represented as threads with ids `0` and `threads + 1` respectively. + */ +internal operator fun ExecutionScenario.get(threadId: Int): List = when (threadId) { + 0 -> initExecution + threads + 1 -> postExecution + else -> parallelExecution[threadId - 1] +} + +/** + * Returns results for the specified thread. Note that initial and post parts + * are represented as threads with ids `0` and `threads + 1` respectively. + */ +internal operator fun ExecutionResult.get(threadId: Int): List = when (threadId) { + 0 -> initResults + parallelResultsWithClock.size + 1 -> postResults + else -> parallelResultsWithClock[threadId - 1].map { it.result } +} + +fun wrapInvalidAccessFromUnnamedModuleExceptionWithDescription(e: Throwable): Throwable { + if (e.message?.contains("to unnamed module") ?: false) { + return RuntimeException(ADD_OPENS_MESSAGE, e) + } + return e +} + +private val ADD_OPENS_MESSAGE = "It seems that you use Java 9+ and the code uses Unsafe or similar constructions that are not accessible from unnamed modules.\n" + + "Please add the following lines to your test running configuration:\n" + + "--add-opens java.base/jdk.internal.misc=ALL-UNNAMED\n" + + "--add-exports java.base/jdk.internal.util=ALL-UNNAMED" + +internal val String.canonicalClassName get() = this.replace('/', '.') +internal val String.internalClassName get() = this.replace('.', '/') + +internal interface Finalizable { + fun finalize() +} + +/** + * Collects the current thread dump and keeps only those + * threads that are related to the specified [runner]. + */ +internal expect fun collectThreadDump(runner: Runner): ThreadDump + +internal expect fun nativeFreeze(any: Any) + +class LincheckAtomicArray(size: Int) { + val array = atomicArrayOfNulls(size) + init { + nativeFreeze(this) + } +} + +class LincheckAtomicIntArray(size: Int) { + val array = AtomicIntArray(size) + init { + nativeFreeze(this) + } +} + +fun LincheckAtomicIntArray.toArray(): IntArray = IntArray(this.array.size) { this.array[it].value } +fun IntArray.toLincheckAtomicIntArray(): LincheckAtomicIntArray { + val ans = LincheckAtomicIntArray(this.size) + for (i in this.indices) { + ans.array[i].value = this[i] + } + return ans +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/Options.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/Options.kt new file mode 100644 index 000000000..df4deb2fb --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/Options.kt @@ -0,0 +1,182 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* +import kotlin.reflect.* + +/** + * Abstract class for test options. + */ +abstract class Options, CTEST : CTestConfiguration> { + internal var logLevel = DEFAULT_LOG_LEVEL + protected var iterations = CTestConfiguration.DEFAULT_ITERATIONS + protected var threads = CTestConfiguration.DEFAULT_THREADS + protected var actorsPerThread = CTestConfiguration.DEFAULT_ACTORS_PER_THREAD + protected var actorsBefore = CTestConfiguration.DEFAULT_ACTORS_BEFORE + protected var actorsAfter = CTestConfiguration.DEFAULT_ACTORS_AFTER + var executionGeneratorGenerator: (testConfiguration: CTestConfiguration, testStructure: CTestStructure) -> ExecutionGenerator = + { testConfiguration, testStructure -> RandomExecutionGenerator(testConfiguration, testStructure) } + var verifierGenerator: (sequentialSpecification: SequentialSpecification<*>) -> Verifier = { sequentialSpecification -> LinearizabilityVerifier(sequentialSpecification) } + protected var requireStateEquivalenceImplementationCheck = false + protected var minimizeFailedScenario = CTestConfiguration.DEFAULT_MINIMIZE_ERROR + var sequentialSpecification: SequentialSpecification<*>? = null + protected var timeoutMs: Long = CTestConfiguration.DEFAULT_TIMEOUT_MS + protected var customScenarios: MutableList = mutableListOf() + + /** + * Number of different test scenarios to be executed + */ + fun iterations(iterations: Int): OPT = applyAndCast { + this.iterations = iterations + } + + /** + * Use the specified number of threads for the parallel part of an execution. + * + * Note, that the the actual number of threads can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.parallelExecution + */ + fun threads(threads: Int): OPT = applyAndCast { + this.threads = threads + } + + /** + * Generate the specified number of operations for each thread of the parallel part of an execution. + * + * Note, that the the actual number of operations can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.parallelExecution + */ + fun actorsPerThread(actorsPerThread: Int): OPT = applyAndCast { + this.actorsPerThread = actorsPerThread + } + + /** + * Generate the specified number of operation for the initial sequential part of an execution. + * + * Note, that the the actual number of operations can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.initExecution + */ + fun actorsBefore(actorsBefore: Int): OPT = applyAndCast { + this.actorsBefore = actorsBefore + } + + /** + * Generate the specified number of operation for the last sequential part of an execution. + * + * Note, that the the actual number of operations can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.postExecution + */ + fun actorsAfter(actorsAfter: Int): OPT = applyAndCast { + this.actorsAfter = actorsAfter + } + + /** + * Use the specified execution generator. + */ + fun executionGenerator(executionGenerator: (testConfiguration: CTestConfiguration, testStructure: CTestStructure) -> ExecutionGenerator): OPT = applyAndCast { + this.executionGeneratorGenerator = executionGenerator + } + + /** + * Use the specified verifier. + */ + fun verifier(verifier: (sequentialSpecification: SequentialSpecification<*>) -> Verifier): OPT = applyAndCast { + this.verifierGenerator = verifier + } + + /** + * Require correctness check of test instance state equivalency relation defined by the user. + * It checks whether two new instances of a test class are equal. + * If the check fails [[IllegalStateException]] is thrown. + */ + fun requireStateEquivalenceImplCheck(require: Boolean): OPT = applyAndCast { + this.requireStateEquivalenceImplementationCheck = require + } + + /** + * If this feature is enabled and an invalid interleaving has been found, + * *lincheck* tries to minimize the corresponding scenario in order to + * construct a smaller one so that the test fails on it as well. + * Enabled by default. + */ + fun minimizeFailedScenario(minimizeFailedScenario: Boolean): OPT = applyAndCast { + this.minimizeFailedScenario = minimizeFailedScenario + } + + abstract fun createTestConfigurations(testClass: TestClass): CTEST + + /** + * Set logging level, [DEFAULT_LOG_LEVEL] is used by default. + */ + fun logLevel(logLevel: LoggingLevel): OPT = applyAndCast { + this.logLevel = logLevel + } + + /** + * The specified class defines the sequential behavior of the testing data structure; + * it is used by [Verifier] to build a labeled transition system, + * and should have the same methods as the testing data structure. + * + * By default, the provided concurrent implementation is used in a sequential way. + */ + fun sequentialSpecification(clazz: SequentialSpecification<*>?): OPT = applyAndCast { + this.sequentialSpecification = clazz + } + + /** + * Examine the specified custom scenario additionally to the generated ones. + */ + fun addCustomScenario(scenario: ExecutionScenario) = applyAndCast { + customScenarios.add(scenario) + } + + /** + * Examine the specified custom scenario additionally to the generated ones. + */ + fun addCustomScenario(scenarioBuilder: ScenarioBuilder.() -> Unit) = + addCustomScenario(scenario { scenarioBuilder() }) + + /** + * Internal, DO NOT USE. + */ + internal fun invocationTimeout(timeoutMs: Long): OPT = applyAndCast { + this.timeoutMs = timeoutMs + } + + companion object { + @Suppress("UNCHECKED_CAST") + internal inline fun , CTEST : CTestConfiguration> Options.applyAndCast( + block: Options.() -> Unit + ) = this.apply { + block() + } as OPT + } +} diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/Result.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/Result.kt new file mode 100644 index 000000000..6c86b20b8 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/Result.kt @@ -0,0 +1,95 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +@file:JvmName("ResultKtCommon") +package org.jetbrains.kotlinx.lincheck + +import kotlin.coroutines.* +import kotlin.jvm.* +import kotlin.reflect.* + +/** + * 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. + * + * [wasSuspended] is true if before getting this result the actor invocation suspended the thread. + * If result is [NoResult] and [wasSuspended] is true it means that + * the execution thread was suspended without any chance to be resumed, + * meaning that all other esealedxecution threads completed their execution or were suspended too. + */ +abstract class Result { + abstract val wasSuspended: Boolean + protected val wasSuspendedPrefix: String get() = (if (wasSuspended) "SUSPENDED + " else "") +} + +/** + * Type of result used if the actor invocation fails with the specified in {@link Operation#handleExceptionsAsResult()} exception [tClazz]. + */ +@Suppress("DataClassPrivateConstructor") +data class ExceptionResult internal constructor(val tClazz: KClass, override val wasSuspended: Boolean) : Result() { + override fun toString() = wasSuspendedPrefix + tClazz.simpleName +} + +/** + * Type of result used if the actor invocation does not return value. + */ +object VoidResult : Result() { + override val wasSuspended get() = false + override fun toString() = wasSuspendedPrefix + VOID +} + +object SuspendedVoidResult : Result() { + override val wasSuspended get() = true + override fun toString() = wasSuspendedPrefix + VOID +} + +private const val VOID = "void" + +object Cancelled : Result() { + override val wasSuspended get() = true + override fun toString() = wasSuspendedPrefix + "CANCELLED" +} + +/** + * Type of result used if the actor invocation suspended the thread and did not get the final result yet + * though it can be resumed later + */ +object NoResult : Result() { + override val wasSuspended get() = false + override fun toString() = "-" +} + +object Suspended : Result() { + override val wasSuspended get() = true + override fun toString() = "S" +} + +/** + * Type of result used for verification. + * Resuming thread writes result of the suspension point and continuation to be executed in the resumed thread into [contWithSuspensionPointRes]. + */ +internal data class ResumedResult(val contWithSuspensionPointRes: Pair?, kotlin.Result>) : Result() { + override val wasSuspended: Boolean get() = true + + lateinit var resumedActor: Actor + lateinit var by: Actor +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/ValueResult.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/ValueResult.kt new file mode 100644 index 000000000..e5ee0e600 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/ValueResult.kt @@ -0,0 +1,10 @@ +package org.jetbrains.kotlinx.lincheck + +/** + * Type of result used if the actor invocation returns any value. + */ +expect class ValueResult : Result { + override fun equals(other: Any?): Boolean + override fun hashCode(): Int + val value: Any? +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt new file mode 100644 index 000000000..4f6842de7 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt @@ -0,0 +1,16 @@ +package org.jetbrains.kotlinx.lincheck.execution + +import org.jetbrains.kotlinx.lincheck.* +import kotlin.random.* +import kotlin.reflect.KClass + +expect class ActorGenerator { + override fun toString(): String + val isSuspendable: Boolean + val useOnce: Boolean + val handledExceptions: List> + + fun generate(threadId: Int): Actor +} + +internal val DETERMINISTIC_RANDOM = Random(42) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionGenerator.java b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionGenerator.kt similarity index 55% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionGenerator.java rename to src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionGenerator.kt index 06dd07f89..51ca66a4e 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionGenerator.java +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionGenerator.kt @@ -1,46 +1,39 @@ -package org.jetbrains.kotlinx.lincheck.execution; - /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * Copyright (C) 2019 - 2021 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.execution -import org.jetbrains.kotlinx.lincheck.CTestConfiguration; -import org.jetbrains.kotlinx.lincheck.CTestStructure; +import org.jetbrains.kotlinx.lincheck.CTestConfiguration +import org.jetbrains.kotlinx.lincheck.CTestStructure /** * Implementation of this interface generates execution scenarios. - * By default, {@link RandomExecutionGenerator} is used. - *

+ * By default, [RandomExecutionGenerator] is used. + * + * * IMPORTANT! - * All implementations should have the same constructor as {@link ExecutionGenerator} has. + * All implementations should have the same constructor as [ExecutionGenerator] has. */ -public abstract class ExecutionGenerator { - protected final CTestConfiguration testConfiguration; - protected final CTestStructure testStructure; - - protected ExecutionGenerator(CTestConfiguration testConfiguration, CTestStructure testStructure) { - this.testConfiguration = testConfiguration; - this.testStructure = testStructure; - } - +abstract class ExecutionGenerator( + protected val testConfiguration: CTestConfiguration, + protected val testStructure: CTestStructure +) { /** * Generates an execution scenario according to the parameters provided by the test configuration * and the restrictions from the test structure. @@ -48,5 +41,5 @@ protected ExecutionGenerator(CTestConfiguration testConfiguration, CTestStructur * If the current test contains suspendable operations, the initial part of an execution * should not contain suspendable actors and the post part should be empty. */ - public abstract ExecutionScenario nextExecution(); -} + abstract fun nextExecution(): ExecutionScenario +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionResult.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionResult.kt similarity index 90% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionResult.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionResult.kt index ddcc5da87..d593f2ebd 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionResult.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionResult.kt @@ -19,6 +19,7 @@ * . * #L% */ + package org.jetbrains.kotlinx.lincheck.execution import org.jetbrains.kotlinx.lincheck.* @@ -57,9 +58,15 @@ data class ExecutionResult( * State representation at the end of the scenario. */ val afterPostStateRepresentation: String? -) { +) : Finalizable { constructor(initResults: List, parallelResultsWithClock: List>, postResults: List) : this(initResults, null, parallelResultsWithClock, null, postResults, null) + + override fun finalize() { + initResults.forEach { if(it is Finalizable) it.finalize() } + parallelResultsWithClock.forEach { l -> l.forEach { if(it is Finalizable) it.finalize() } } + postResults.forEach { if(it is Finalizable) it.finalize() } + } } val ExecutionResult.withEmptyClocks: ExecutionResult get() = ExecutionResult( diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionScenario.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionScenario.kt new file mode 100644 index 000000000..c8256c724 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionScenario.kt @@ -0,0 +1,117 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ +@file:JvmName("ExecutionScenarioKtCommon") + +package org.jetbrains.kotlinx.lincheck.execution + +import org.jetbrains.kotlinx.lincheck.Actor +import kotlin.jvm.* + +/** + * This class represents an execution scenario, which + * is generated by an [ExecutionGenerator] and then + * used by a [Strategy] which produces an [ExecutionResult]. + */ +class ExecutionScenario( + /** + * The initial sequential part of the execution. + * It helps to produce different initial states + * before the parallel part. + * + * The initial execution part should contain only non-suspendable actors; + * otherwise, the single initial execution thread will suspend with no chance to be resumed. + */ + val initExecution: List, + /** + * The parallel part of the execution, which is used + * to find an interleaving with incorrect behaviour. + */ + val parallelExecution: List>, + /** + * The last sequential part is used to test that + * the data structure is in some correct state. + * + * If this execution scenario contains suspendable actors, the post part should be empty; + * if not, an actor could resume a previously suspended one from the parallel execution part. + */ + val postExecution: List +) { + override fun toString(): String { + val sb = StringBuilder() + sb.appendExecutionScenario(this) + return sb.toString() + } + + fun finalize() { + initExecution.forEach { it.finalize() } + parallelExecution.forEach { l -> l.forEach { it.finalize() } } + postExecution.forEach { it.finalize() } + } +} + +/** + * Returns the number of threads used in the parallel part of this execution. + */ +val ExecutionScenario.threads: Int + get() = parallelExecution.size + +/** + * Returns `true` if there is at least one suspendable actor in the generated scenario + */ +fun ExecutionScenario.hasSuspendableActors() = parallelExecution.any { actors -> actors.any { it.isSuspendable } } || postExecution.any { it.isSuspendable } + +internal fun printInColumnsCustom( + groupedObjects: List>, + joinColumns: (List) -> String +): String { + val nRows = groupedObjects.map { it.size }.max() ?: 0 + val nColumns = groupedObjects.size + val rows = (0 until nRows).map { rowIndex -> + (0 until nColumns) + .map { groupedObjects[it] } + .map { it.getOrNull(rowIndex)?.toString().orEmpty() } // print empty strings for empty cells + } + val columnWidths: List = (0 until nColumns).map { columnIndex -> + (0 until nRows).map { rowIndex -> rows[rowIndex][columnIndex].length }.max() ?: 0 + } + return (0 until nRows) + .map { rowIndex -> rows[rowIndex].mapIndexed { columnIndex, cell -> cell.padEnd(columnWidths[columnIndex]) } } + .map { rowCells -> joinColumns(rowCells) } + .joinToString(separator = "\n") +} + +internal fun printInColumns(groupedObjects: List>) = printInColumnsCustom(groupedObjects) { it.joinToString(separator = " | ", prefix = "| ", postfix = " |") } + +internal fun StringBuilder.appendExecutionScenario(scenario: ExecutionScenario): StringBuilder { + if (scenario.initExecution.isNotEmpty()) { + appendLine("Execution scenario (init part):") + appendLine(scenario.initExecution) + } + if (scenario.parallelExecution.isNotEmpty()) { + appendLine("Execution scenario (parallel part):") + append(printInColumns(scenario.parallelExecution)) + appendLine() + } + if (scenario.postExecution.isNotEmpty()) { + appendLine("Execution scenario (post part):") + append(scenario.postExecution) + } + return this +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/HBClock.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/HBClock.kt similarity index 60% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/HBClock.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/execution/HBClock.kt index 9f8bb31ef..b90b80794 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/HBClock.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/HBClock.kt @@ -1,51 +1,54 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.execution -import org.jetbrains.kotlinx.lincheck.Result +import org.jetbrains.kotlinx.lincheck.* -data class HBClock(val clock: IntArray) { - val threads: Int get() = clock.size - val empty: Boolean get() = clock.all { it == 0 } - operator fun get(i: Int) = clock[i] +data class HBClock(val clock: LincheckAtomicIntArray) { + val threads: Int get() = clock.array.size + val empty: Boolean get() = clock.toArray().all { it == 0 } + operator fun get(i: Int) = clock.array[i].value - override fun toString() = clock.joinToString(prefix = "[", separator = ",", postfix = "]") { + override fun toString() = clock.toArray().joinToString(prefix = "[", separator = ",", postfix = "]") { if (it == 0) "-" else "$it" } override fun equals(other: Any?): Boolean { if (this === other) return true - if (javaClass != other?.javaClass) return false + if (other != null && this::class != other::class) return false other as HBClock - return clock.contentEquals(other.clock) + return clock.toArray().contentEquals(other.clock.toArray()) } - override fun hashCode() = clock.contentHashCode() + override fun hashCode() = clock.toArray().contentHashCode() } fun emptyClock(size: Int) = HBClock(emptyClockArray(size)) -fun emptyClockArray(size: Int) = IntArray(size) { 0 } +fun emptyClockArray(size: Int) = LincheckAtomicIntArray(size) -data class ResultWithClock(val result: Result, val clockOnStart: HBClock) +data class ResultWithClock(val result: Result, val clockOnStart: HBClock) : Finalizable { + override fun finalize() { + if(result is Finalizable) result.finalize() + } +} fun Result.withEmptyClock(threads: Int) = ResultWithClock(this, emptyClock(threads)) fun List.withEmptyClock(threads: Int): List = map { it.withEmptyClock(threads) } diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/execution/RandomExecutionGenerator.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/RandomExecutionGenerator.kt new file mode 100644 index 000000000..37cf9d9ed --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/execution/RandomExecutionGenerator.kt @@ -0,0 +1,103 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ +package org.jetbrains.kotlinx.lincheck.execution + +import org.jetbrains.kotlinx.lincheck.* +import kotlin.random.Random + +class RandomExecutionGenerator(testConfiguration: CTestConfiguration, testStructure: CTestStructure) : ExecutionGenerator(testConfiguration, testStructure) { + private val random = Random(0) + override fun nextExecution(): ExecutionScenario { + // Create init execution part + val validActorGeneratorsForInit = testStructure.actorGenerators.filter { ag: ActorGenerator -> !ag.useOnce && !ag.isSuspendable } + val initExecution: MutableList = ArrayList() + run { + var i = 0 + while (i < testConfiguration.actorsBefore && validActorGeneratorsForInit.isNotEmpty()) { + val ag = validActorGeneratorsForInit[random.nextInt(validActorGeneratorsForInit.size)] + initExecution.add(ag.generate(0)) + i++ + } + } + // Create parallel execution part + // Construct non-parallel groups and parallel one + val nonParallelGroups = testStructure.operationGroups.filter { g: OperationGroup -> g.nonParallel }.shuffled() + val parallelGroup: MutableList = ArrayList(testStructure.actorGenerators) + nonParallelGroups.forEach { g: OperationGroup -> parallelGroup.removeAll(g.actors) } + val parallelExecution: MutableList> = ArrayList() + val threadGens: MutableList = ArrayList() + for (t in 0 until testConfiguration.threads) { + parallelExecution.add(ArrayList()) + threadGens.add(ThreadGen(t, testConfiguration.actorsPerThread)) + } + for (i in nonParallelGroups.indices) { + threadGens[i % threadGens.size].nonParallelActorGenerators + .addAll(nonParallelGroups[i].actors) + } + val tgs2: List = ArrayList(threadGens) + while (threadGens.isNotEmpty()) { + val it = threadGens.iterator() + while (it.hasNext()) { + val threadGen = it.next() + val aGenIndexBound = threadGen.nonParallelActorGenerators.size + parallelGroup.size + if (aGenIndexBound == 0) { + it.remove() + continue + } + val aGenIndex = random.nextInt(aGenIndexBound) + val agen: ActorGenerator = if (aGenIndex < threadGen.nonParallelActorGenerators.size) { + getActorGenFromGroup(threadGen.nonParallelActorGenerators, aGenIndex) + } else { + getActorGenFromGroup(parallelGroup, + aGenIndex - threadGen.nonParallelActorGenerators.size) + } + parallelExecution[threadGen.iThread].add(agen.generate(threadGen.iThread + 1)) + if (--threadGen.left == 0) it.remove() + } + } + parallelExecution.retainAll { actors: List -> actors.isNotEmpty() } + // Create post execution part if the parallel part does not have suspendable actors + val postExecution: MutableList + if (parallelExecution.none { actors: List -> actors.any(Actor::isSuspendable) }) { + postExecution = ArrayList() + val leftActorGenerators: MutableList = ArrayList(parallelGroup) + for (threadGen in tgs2) leftActorGenerators.addAll(threadGen.nonParallelActorGenerators) + var i = 0 + while (i < testConfiguration.actorsAfter && leftActorGenerators.isNotEmpty()) { + val agen = getActorGenFromGroup(leftActorGenerators, random.nextInt(leftActorGenerators.size)) + postExecution.add(agen.generate(testConfiguration.threads + 1)) + i++ + } + } else { + postExecution = arrayListOf() + } + return ExecutionScenario(initExecution, parallelExecution, postExecution) + } + + private fun getActorGenFromGroup(aGens: List, index: Int): ActorGenerator { + val aGen = aGens[index] + if (aGen.useOnce) (aGens as MutableList).removeAt(index) + return aGen + } + + private class ThreadGen(var iThread: Int, var left: Int) { + val nonParallelActorGenerators: MutableList = ArrayList() + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/BooleanGen.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/BooleanGen.kt similarity index 95% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/BooleanGen.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/BooleanGen.kt index 5ed2a13c6..81255f888 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/BooleanGen.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/BooleanGen.kt @@ -1,7 +1,7 @@ /* * Lincheck * - * Copyright (C) 2019 - 2020 JetBrains s.r.o. + * Copyright (C) 2019 - 2021 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 diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/FloatGen.java b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ByteGen.kt similarity index 59% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/FloatGen.java rename to src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ByteGen.kt index 6d2417ea9..42019099b 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/FloatGen.java +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ByteGen.kt @@ -1,35 +1,32 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * Copyright (C) 2019 - 2021 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.paramgen -public class FloatGen implements ParameterGenerator { - private final DoubleGen doubleGen; +class ByteGen(configuration: String) : ParameterGenerator { + private val intGen: IntGen = IntGen(configuration) - public FloatGen(String configuration) { - doubleGen = new DoubleGen(configuration); + init { + intGen.checkRange(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt(), "byte") } - public Float generate() { - return (float) (double) doubleGen.generate(); + override fun generate(): Byte { + return intGen.generate().toByte() } } \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/DoubleGen.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/DoubleGen.kt new file mode 100644 index 000000000..743cdaa5c --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/DoubleGen.kt @@ -0,0 +1,66 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ +package org.jetbrains.kotlinx.lincheck.paramgen + +import kotlin.random.Random + +class DoubleGen(configuration: String) : ParameterGenerator { + private val random = Random(0) + private var begin = 0.0 + private var end = 0.0 + private var step = 0.0 + + init { + if (configuration.isEmpty()) { // use default configuration + begin = DEFAULT_BEGIN.toDouble() + end = DEFAULT_END.toDouble() + step = DEFAULT_STEP.toDouble() + } else { + val args = configuration.replace("\\s".toRegex(), "").split(":".toRegex()).toTypedArray() + when (args.size) { + 2 -> { + begin = args[0].toDouble() + end = args[1].toDouble() + step = DEFAULT_STEP.toDouble() + } + 3 -> { + begin = args[0].toDouble() + step = args[1].toDouble() + end = args[2].toDouble() + } + else -> throw IllegalArgumentException("Configuration should have two (begin and end) " + + "or three (begin, step and end) arguments separated by colon") + } + require((end - begin) / step < Int.MAX_VALUE) { "step is too small for specified range" } + } + } + + override fun generate(): Double { + val delta = end - begin + if (step == 0.0) // step is not defined + return begin + delta * random.nextDouble() + val maxSteps = (delta / step).toInt() + return begin + delta * random.nextInt(maxSteps + 1) + } +} + +private const val DEFAULT_BEGIN = -10f +private const val DEFAULT_END = 10f +private const val DEFAULT_STEP = 0.1f \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/LongGen.java b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/FloatGen.kt similarity index 60% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/LongGen.java rename to src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/FloatGen.kt index fa9e71b09..124c039d2 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/LongGen.java +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/FloatGen.kt @@ -1,35 +1,28 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * Copyright (C) 2019 - 2021 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.paramgen -public class LongGen implements ParameterGenerator { - private final IntGen intGen; - - public LongGen(String configuration) { - intGen = new IntGen(configuration); - } +class FloatGen(configuration: String) : ParameterGenerator { + private val doubleGen: DoubleGen = DoubleGen(configuration) - public Long generate() { - return (long) intGen.generate(); + override fun generate(): Float { + return doubleGen.generate().toFloat() } -} +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/IntGen.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/IntGen.kt new file mode 100644 index 000000000..d27e7eb3a --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/IntGen.kt @@ -0,0 +1,59 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ +package org.jetbrains.kotlinx.lincheck.paramgen + +import kotlin.random.Random + +class IntGen(configuration: String) : ParameterGenerator { + private val random = Random(0) + private var begin = 0 + private var end = 0 + + init { + if (configuration.isEmpty()) { // use default configuration + begin = DEFAULT_BEGIN + end = DEFAULT_END + } else { + val args = configuration.replace("\\s".toRegex(), "").split(":".toRegex()).toTypedArray() + when (args.size) { + 2 -> { + begin = args[0].toInt() + end = args[1].toInt() + } + else -> throw IllegalArgumentException("Configuration should have " + + "two arguments (begin and end) separated by colon") + } + } + } + + override fun generate(): Int { + return begin + random.nextInt(end - begin + 1) + } + + fun checkRange(min: Int, max: Int, type: String) { + require(!(begin < min || end - 1 > max)) { + ("Illegal range for " + + type + " type: [" + begin + "; " + end + ")") + } + } +} + +private const val DEFAULT_BEGIN = -10 +private const val DEFAULT_END = 10 \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/LongGen.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/LongGen.kt new file mode 100644 index 000000000..d2d7719b3 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/LongGen.kt @@ -0,0 +1,28 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ +package org.jetbrains.kotlinx.lincheck.paramgen + +class LongGen(configuration: String) : ParameterGenerator { + private val intGen: IntGen = IntGen(configuration) + + override fun generate(): Long { + return intGen.generate().toLong() + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ParameterGenerator.java b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ParameterGenerator.kt similarity index 59% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ParameterGenerator.java rename to src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ParameterGenerator.kt index 4c6b9530a..ca286c8da 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ParameterGenerator.java +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ParameterGenerator.kt @@ -1,40 +1,35 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * 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.paramgen -import org.jetbrains.kotlinx.lincheck.annotations.Operation; /** * The implementation of this interface is used to generate parameters - * for {@link Operation operation}. - */ -public interface ParameterGenerator { - T generate(); + * for [operation][Operation]. + */ +interface ParameterGenerator { + fun generate(): T - final class Dummy implements ParameterGenerator { - @Override - public Object generate() { - throw new UnsupportedOperationException(); + class Dummy : ParameterGenerator { + override fun generate(): Any { + throw UnsupportedOperationException() } } -} +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ShortGen.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ShortGen.kt new file mode 100644 index 000000000..ac70e154d --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ShortGen.kt @@ -0,0 +1,54 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.paramgen + +/* + * #%L + * Lincheck + * %% + * Copyright (C) 2015 - 2018 Devexperts, LLC + * %% + * 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% + */ +class ShortGen(configuration: String) : ParameterGenerator { + private val intGen: IntGen = IntGen(configuration) + + init { + intGen.checkRange(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt(), "short") + } + + override fun generate(): Short { + return intGen.generate().toShort() + } +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/StringGen.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/StringGen.kt new file mode 100644 index 000000000..181daaa60 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/StringGen.kt @@ -0,0 +1,53 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ +package org.jetbrains.kotlinx.lincheck.paramgen + +import kotlin.random.* + +class StringGen(configuration: String) : ParameterGenerator { + private val random = Random(0) + private var maxWordLength = 0 + private var alphabet: String? = null + + init { + if (configuration.isEmpty()) { // use default configuration + maxWordLength = DEFAULT_MAX_WORD_LENGTH + alphabet = DEFAULT_ALPHABET + } else { + val firstCommaIndex = configuration.indexOf(':') + if (firstCommaIndex < 0) { // maxWordLength only + maxWordLength = configuration.toInt() + alphabet = DEFAULT_ALPHABET + } else { // maxWordLength:alphabet + maxWordLength = configuration.substring(0, firstCommaIndex).toInt() + alphabet = configuration.substring(firstCommaIndex + 1) + } + } + } + + override fun generate(): String { + val cs = CharArray(random.nextInt(maxWordLength)) + for (i in cs.indices) cs[i] = alphabet!![random.nextInt(alphabet!!.length)] + return cs.concatToString() + } +} + +private const val DEFAULT_MAX_WORD_LENGTH = 15 +private const val DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ " \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ThreadIdGen.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ThreadIdGen.kt similarity index 89% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ThreadIdGen.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ThreadIdGen.kt index 9745350c7..b748dce4c 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ThreadIdGen.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/paramgen/ThreadIdGen.kt @@ -1,23 +1,21 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.paramgen diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt new file mode 100644 index 000000000..e69de29bb diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/runner/InvocationResult.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/runner/InvocationResult.kt new file mode 100644 index 000000000..2fb7adccd --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/runner/InvocationResult.kt @@ -0,0 +1,76 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.runner + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.Finalizable +import org.jetbrains.kotlinx.lincheck.execution.* + +expect class ThreadDump + +/** + * Represents results for invocations, see [Runner.run]. + */ +open class InvocationResult + +/** + * The invocation completed successfully, the output [results] are provided. + */ +class CompletedInvocationResult( + val results: ExecutionResult +) : InvocationResult(), Finalizable { + override fun finalize() { + results.finalize() + } +} + +/** + * The invocation has completed with an unexpected exception. + */ +class UnexpectedExceptionInvocationResult( + val exception: Throwable +) : InvocationResult() + +/** + * The invocation successfully completed, but the + * [validation function][org.jetbrains.kotlinx.lincheck.annotations.Validate] + * check failed. + */ +class ValidationFailureInvocationResult( + val scenario: ExecutionScenario, + val functionName: String, + val exception: Throwable +) : InvocationResult() + +/** + * Obstruction freedom check is requested, + * but an invocation that hangs has been found. + */ +class ObstructionFreedomViolationInvocationResult( + val reason: String +) : InvocationResult() + +/** + * Indicates that the invocation has run into deadlock or livelock. + */ +class DeadlockInvocationResult( + val threadDump: ThreadDump +) : InvocationResult() \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt new file mode 100644 index 000000000..8a520bf54 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt @@ -0,0 +1,51 @@ +/*- + * #%L + * Lincheck + * %% + * Copyright (C) 2019 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.runner + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.strategy.* + +/** + * This runner executes parallel scenario part in different threads. + * Supports running scenarios with `suspend` functions. + * + * It is pretty useful for stress testing or if you do not care about context switch expenses. + */ +internal expect open class ParallelThreadsRunner( + strategy: Strategy, + testClass: TestClass, + validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, + timeoutMs: Long, // for deadlock or livelock detection + useClocks: UseClocks, // specifies whether `HBClock`-s should always be used or with some probability + initThreadFunction: (() -> Unit)? = null, + finishThreadFunction: (() -> Unit)? = null +) : Runner + +internal class LincheckExecutionException(cause: Throwable?): Exception(cause) + +internal class LincheckTimeoutException: Exception() + +internal enum class UseClocks { ALWAYS, RANDOM } + +internal enum class CompletionStatus { CANCELLED, RESUMED } \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt new file mode 100644 index 000000000..dfdb01888 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt @@ -0,0 +1,167 @@ +/*- + * #%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.runner + +import kotlinx.atomicfu.* +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.execution.* +import kotlin.contracts.* + +expect fun Runner.loadClassLoader(): Any +expect fun loadClass(classLoader: Any, testClass: TestClass): TestClass + +expect class AtomicInteger(value: Int) { + fun get(): Int + fun set(value: Int) +} + +/** + * Runner determines how to run your concurrent test. In order to support techniques + * like fibers, it may require code transformation, so that [createTransformer] should + * provide the corresponding transformer and [needsTransformation] should return `true`. + */ +abstract class Runner protected constructor( + val strategy: Strategy, + private val _testClass: TestClass, // will be transformed later + protected val validationFunctions: List, + protected val stateRepresentationFunction: StateRepresentationFunction? +) : Finalizable { + protected var scenario = strategy.scenario // `strategy.scenario` will be transformed in `initialize` + protected lateinit var testClass: TestClass // not available before `initialize` call + @Suppress("LeakingThis") + val classLoader: Any = loadClassLoader() + protected val completedOrSuspendedThreads = AtomicInteger(0) + + /** + * This method is a part of `Runner` initialization and should be invoked after this runner + * creation. It is separated from the constructor to perform the strategy initialization at first. + */ + open fun initialize() { + scenario = strategy.scenario.convertForLoader(classLoader) + testClass = loadClass(_testClass) + } + + /** + * Returns the current state representation of the test instance constructed via + * the function marked with [StateRepresentation] annotation, or `null` + * if no such function is provided. + * + * Please note, that it is unsafe to call this method concurrently with the running scenario. + * However, it is fine to call it if the execution is paused somewhere in the middle. + */ + open fun constructStateRepresentation(): String? = null + + /** + * Loads the specified class via this runner' class loader. + */ + private fun loadClass(testClass: TestClass): TestClass = loadClass(classLoader, testClass) + + /** + * Creates a transformer required for this runner. + * Throws [UnsupportedOperationException] by default. + * + * @param cv should be of type ClassVisitor + * @return class visitor which transform the code due to support this runner. + */ + open fun createTransformer(cv: Any): Any? = null + + /** + * This method should return `true` if code transformation + * is required for this runner; returns `false` by default. + */ + open fun needsTransformation(): Boolean = false + + /** + * Runs the next invocation. + */ + abstract fun run(): InvocationResult + + /** + * This method is invoked by every test thread as the first operation. + * @param iThread number of invoking thread + */ + open fun onStart(iThread: Int) {} + + /** + * This method is invoked by every test thread as the last operation + * if no exception has been thrown. + * @param iThread number of invoking thread + */ + open fun onFinish(iThread: Int) {} + + /** + * This method is invoked by the corresponding test thread + * when an unexpected exception is thrown. + */ + open fun onFailure(iThread: Int, e: Throwable) {} + + /** + * This method is invoked by the corresponding test thread + * when the current coroutine suspends. + * @param iThread number of invoking thread + */ + open fun afterCoroutineSuspended(iThread: Int): Unit = throw UnsupportedOperationException("Coroutines are not supported") + + /** + * This method is invoked by the corresponding test thread + * when the current coroutine is resumed. + */ + open fun afterCoroutineResumed(iThread: Int): Unit = throw UnsupportedOperationException("Coroutines are not supported") + + /** + * This method is invoked by the corresponding test thread + * when the current coroutine is cancelled. + */ + open fun afterCoroutineCancelled(iThread: Int): Unit = throw UnsupportedOperationException("Coroutines are not supported") + + /** + * Returns `true` if the coroutine corresponding to + * the actor `actorId` in the thread `iThread` is resumed. + */ + open fun isCoroutineResumed(iThread: Int, actorId: Int): Boolean = throw UnsupportedOperationException("Coroutines are not supported") + + /** + * Is invoked before each actor execution from the specified thread. + * The invocations are inserted into the generated code. + */ + fun onActorStart(iThread: Int) { + strategy.onActorStart(iThread) + } + + /** + * Closes the resources used in this runner. + */ + open fun close() {} + + /** + * @return whether all scenario threads are completed or suspended + * Used by generated code. + */ + val isParallelExecutionCompleted: Boolean + get() = completedOrSuspendedThreads.get() == scenario.threads + + override fun finalize() { + scenario.finalize() + } +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/LincheckFailure.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/LincheckFailure.kt new file mode 100644 index 000000000..38e213a36 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/LincheckFailure.kt @@ -0,0 +1,74 @@ +/*- + * #%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.strategy + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.runner.* + +expect class Trace + +open class LincheckFailure( + val scenario: ExecutionScenario, + val trace: Trace? +) { + override fun toString() = StringBuilder().appendFailure(this).toString() +} + +internal class IncorrectResultsFailure( + scenario: ExecutionScenario, + val results: ExecutionResult, + trace: Trace? = null +) : LincheckFailure(scenario, trace) + +internal class DeadlockWithDumpFailure( + scenario: ExecutionScenario, + val threadDump: ThreadDump, + trace: Trace? = null +) : LincheckFailure(scenario, trace) + +internal class UnexpectedExceptionFailure( + scenario: ExecutionScenario, + val exception: Throwable, + trace: Trace? = null +) : LincheckFailure(scenario, trace) + +internal class ValidationFailure( + scenario: ExecutionScenario, + val functionName: String, + val exception: Throwable, + trace: Trace? = null +) : LincheckFailure(scenario, trace) + +internal class ObstructionFreedomViolationFailure( + scenario: ExecutionScenario, + val reason: String, + trace: Trace? = null +) : LincheckFailure(scenario, trace) + +internal fun InvocationResult.toLincheckFailure(scenario: ExecutionScenario, trace: Trace? = null) = when (this) { + is DeadlockInvocationResult -> DeadlockWithDumpFailure(scenario, threadDump, trace) + is UnexpectedExceptionInvocationResult -> UnexpectedExceptionFailure(scenario, exception, trace) + is ValidationFailureInvocationResult -> ValidationFailure(scenario, functionName, exception, trace) + is ObstructionFreedomViolationInvocationResult -> ObstructionFreedomViolationFailure(scenario, reason, trace) + else -> error("Unexpected invocation result type: ${this::class.simpleName}") +} \ No newline at end of file diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt new file mode 100644 index 000000000..c8da2f52c --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt @@ -0,0 +1,41 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.strategy + +import org.jetbrains.kotlinx.lincheck.execution.* + +/** + * Implementation of this class describes how to run the generated execution. + * + * Note that strategy can run execution several times. For strategy creating + * [.createStrategy] method is used. It is impossible to add a new strategy + * without any code change. + */ +expect abstract class Strategy protected constructor(scenario: ExecutionScenario) { + val scenario: ExecutionScenario + + abstract fun run(): LincheckFailure? + + /** + * Is invoked before each actor execution. + */ + open fun onActorStart(iThread: Int) +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt similarity index 50% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt index 668c21900..fd5fbde72 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTestConfiguration.kt @@ -24,20 +24,31 @@ package org.jetbrains.kotlinx.lincheck.strategy.stress import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.verifier.* -import java.lang.reflect.* /** * Configuration for [stress][StressStrategy] strategy. */ -class StressCTestConfiguration(testClass: Class<*>, iterations: Int, threads: Int, actorsPerThread: Int, actorsBefore: Int, actorsAfter: Int, - generatorClass: Class, verifierClass: Class, - val invocationsPerIteration: Int, requireStateEquivalenceCheck: Boolean, minimizeFailedScenario: Boolean, - sequentialSpecification: Class<*>, timeoutMs: Long, customScenarios: List -) : CTestConfiguration(testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, generatorClass, verifierClass, +class StressCTestConfiguration(testClass: TestClass, + iterations: Int, + threads: Int, + actorsPerThread: Int, + actorsBefore: Int, + actorsAfter: Int, + executionGenerator: (testConfiguration: CTestConfiguration, testStructure: CTestStructure) -> ExecutionGenerator, + verifierGenerator: (sequentialSpecification: SequentialSpecification<*>) -> Verifier, + val invocationsPerIteration: Int, + requireStateEquivalenceCheck: Boolean, + minimizeFailedScenario: Boolean, + sequentialSpecification: SequentialSpecification<*>, + timeoutMs: Long, + customScenarios: List, + val initThreadFunction: (() -> Unit)? = null, + val finishThreadFunction: (() -> Unit)? = null +) : CTestConfiguration(testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, executionGenerator, verifierGenerator, requireStateEquivalenceCheck, minimizeFailedScenario, sequentialSpecification, timeoutMs, customScenarios) { - override fun createStrategy(testClass: Class<*>, scenario: ExecutionScenario, validationFunctions: List, - stateRepresentationMethod: Method?, verifier: Verifier) = - StressStrategy(this, testClass, scenario, validationFunctions, stateRepresentationMethod, verifier) + override fun createStrategy(testClass: TestClass, scenario: ExecutionScenario, validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, verifier: Verifier) = + StressStrategy(this, testClass, scenario, validationFunctions, stateRepresentationFunction, verifier) companion object { const val DEFAULT_INVOCATIONS = 10000 diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressOptions.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressOptions.kt similarity index 52% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressOptions.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressOptions.kt index c112745fe..49d611d24 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressOptions.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressOptions.kt @@ -1,34 +1,33 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.strategy.stress -import org.jetbrains.kotlinx.lincheck.Options -import org.jetbrains.kotlinx.lincheck.chooseSequentialSpecification +import org.jetbrains.kotlinx.lincheck.* /** * Options for [stress][StressStrategy] strategy. */ open class StressOptions : Options() { - private var invocationsPerIteration = StressCTestConfiguration.DEFAULT_INVOCATIONS + protected var invocationsPerIteration = StressCTestConfiguration.DEFAULT_INVOCATIONS + protected var initThreadFunction: (() -> Unit)? = null + protected var finishThreadFunction: (() -> Unit)? = null /** * Run each test scenario the specified number of times. @@ -37,9 +36,23 @@ open class StressOptions : Options() { invocationsPerIteration = invocations } - override fun createTestConfigurations(testClass: Class<*>): StressCTestConfiguration { - return StressCTestConfiguration(testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, executionGenerator, - verifier, invocationsPerIteration, requireStateEquivalenceImplementationCheck, minimizeFailedScenario, - chooseSequentialSpecification(sequentialSpecification, testClass), timeoutMs, customScenarios) + /** + * Setup init function that will be invoked once in every worker thread before operations. + */ + fun initThreadFunction(function: () -> Unit): StressOptions = apply { + initThreadFunction = function + } + + /** + * Setup finish function that will be invoked once in every worker thread after operations. + */ + fun finishThreadFunction(function: () -> Unit): StressOptions = apply { + finishThreadFunction = function + } + + override fun createTestConfigurations(testClass: TestClass): StressCTestConfiguration { + return StressCTestConfiguration(testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, executionGeneratorGenerator, + verifierGenerator, invocationsPerIteration, requireStateEquivalenceImplementationCheck, minimizeFailedScenario, + chooseSequentialSpecification(sequentialSpecification, testClass), timeoutMs, customScenarios, initThreadFunction, finishThreadFunction) } } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/LogLevel.java b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt similarity index 52% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/LogLevel.java rename to src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt index f313822ea..f87c5c1fe 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/LogLevel.java +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt @@ -1,10 +1,8 @@ -package org.jetbrains.kotlinx.lincheck.annotations; - -/* +/*- * #%L * Lincheck * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC + * 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 @@ -21,22 +19,21 @@ * . * #L% */ +package org.jetbrains.kotlinx.lincheck.strategy.stress -import org.jetbrains.kotlinx.lincheck.LoggingLevel; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.runner.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.verifier.* -/** - * This annotation should be added to a test class to specify the logging level. - * By default, {@link LoggingLevel#WARN} is used. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Inherited -public @interface LogLevel { - LoggingLevel value(); -} +expect class StressStrategy( + testCfg: StressCTestConfiguration, + testClass: TestClass, + scenario: ExecutionScenario, + validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, + verifier: Verifier +) : Strategy { + override fun run(): LincheckFailure? +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/AbstractLTSVerifier.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/AbstractLTSVerifier.kt similarity index 96% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/AbstractLTSVerifier.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/verifier/AbstractLTSVerifier.kt index da4baf1f9..7bf8a03f0 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/AbstractLTSVerifier.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/AbstractLTSVerifier.kt @@ -23,7 +23,7 @@ package org.jetbrains.kotlinx.lincheck.verifier import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* -import java.util.ArrayList +import kotlin.reflect.* /** * An abstraction for verifiers which use the labeled transition system (LTS) under the hood. @@ -34,7 +34,7 @@ import java.util.ArrayList * the next possible transitions using [VerifierContext.nextContext] function. This verifier * uses depth-first search to find a proper path. */ -abstract class AbstractLTSVerifier(protected val sequentialSpecification: Class<*>) : CachedVerifier() { +abstract class AbstractLTSVerifier(protected val sequentialSpecification: SequentialSpecification<*>) : CachedVerifier() { abstract val lts: LTS abstract fun createInitialContext(scenario: ExecutionScenario, results: ExecutionResult): VerifierContext @@ -52,7 +52,7 @@ abstract class AbstractLTSVerifier(protected val sequentialSpecification: Class< return false } - override fun checkStateEquivalenceImplementation() = lts.checkStateEquivalenceImplementation() + override fun checkStateEquivalenceImplementation(): Boolean = lts.checkStateEquivalenceImplementation() } /** diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/EpsilonVerifier.java b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/EpsilonVerifier.kt similarity index 54% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/EpsilonVerifier.java rename to src/common/main/org/jetbrains/kotlinx/lincheck/verifier/EpsilonVerifier.kt index 2462cfd64..35c13c98e 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/EpsilonVerifier.java +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/EpsilonVerifier.kt @@ -1,9 +1,8 @@ /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * Copyright (C) 2019 - 2021 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 @@ -16,27 +15,21 @@ * * 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.verifier; +package org.jetbrains.kotlinx.lincheck.verifier -import org.jetbrains.kotlinx.lincheck.execution.*; +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario /** * This verifier does nothing and could be used for performance benchmarking. */ -public class EpsilonVerifier implements Verifier { - - public EpsilonVerifier(Class sequentialSpecification) {} - - @Override - public boolean verifyResults(ExecutionScenario scenario, ExecutionResult results) { - return true; // Always correct results :) - } +class EpsilonVerifier(sequentialSpecification: SequentialSpecification<*>) : Verifier { + override fun verifyResults(scenario: ExecutionScenario, results: ExecutionResult): Boolean = true // Always correct results :) - @Override - public boolean checkStateEquivalenceImplementation() { - return true; + override fun checkStateEquivalenceImplementation(): Boolean { + return true } -} +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt similarity index 91% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt index 22b047e9a..8b3917be1 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/LTS.kt @@ -1,9 +1,8 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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 @@ -16,21 +15,19 @@ * * 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.verifier import kotlinx.coroutines.* import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.CancellableContinuationHolder.storedLastCancellableCont import org.jetbrains.kotlinx.lincheck.verifier.LTS.* import org.jetbrains.kotlinx.lincheck.verifier.OperationType.* -import java.util.* import kotlin.collections.HashMap import kotlin.coroutines.* import kotlin.math.* +import kotlin.reflect.* typealias RemappingFunction = IntArray typealias ResumedTickets = Set @@ -55,10 +52,9 @@ typealias ResumedTickets = Set * Practically, Kotlin implementation of such operations via suspend functions is supported. */ -class LTS(sequentialSpecification: Class<*>) { +class LTS(sequentialSpecification: SequentialSpecification<*>) { // we should transform the specification with `CancellabilitySupportClassTransformer` - private val sequentialSpecification: Class<*> = TransformationClassLoader { cv -> CancellabilitySupportClassTransformer(cv)} - .loadClass(sequentialSpecification.name)!! + private val sequentialSpecification: SequentialSpecification = loadSequentialSpecification(sequentialSpecification) /** * Cache with all LTS states in order to reuse the equivalent ones. @@ -122,7 +118,7 @@ class LTS(sequentialSpecification: Class<*>) { } } check(transitionInfo.result != Suspended) { - "Execution of the follow-up part of this operation ${actor.method} suspended - this behaviour is not supported" + "Execution of the follow-up part of this operation ${actor} suspended - this behaviour is not supported" } return if (expectedResult.isLegalByFollowUp(transitionInfo, actor.allowExtraSuspension)) transitionInfo else null } @@ -194,7 +190,7 @@ class LTS(sequentialSpecification: Class<*>) { private fun getResumedOperations(resumedTicketsWithResults: Map): List { val resumedOperations = mutableListOf() - resumedTicketsWithResults.forEach { resumedTicket, res -> + resumedTicketsWithResults.forEach { (resumedTicket, res) -> resumedOperations.add(ResumptionInfo(res.resumedActor, res.by, resumedTicket)) } // Ignore the order of resumption by sorting the list of resumptions. @@ -225,7 +221,7 @@ class LTS(sequentialSpecification: Class<*>) { } CANCELLATION -> { continuationsMap[Operation(this.actor, this.ticket, REQUEST)]!!.cancelByLincheck(promptCancellation = actor.promptCancellation) - val wasSuspended = suspendedOperations.removeIf { it.actor == actor && it.ticket == ticket } + val wasSuspended = suspendedOperations.removeAll { it.actor == actor && it.ticket == ticket } if (!actor.promptCancellation) { check(wasSuspended) { "The operation can be cancelled after resumption only in the prompt cancellation mode" } } @@ -244,7 +240,7 @@ class LTS(sequentialSpecification: Class<*>) { } resumedOperations.forEach { (resumedTicket, res) -> if (!prevResumedTickets.contains(resumedTicket)) { - suspendedOperations.removeIf { it.ticket == resumedTicket } + suspendedOperations.removeAll { it.ticket == resumedTicket } res.by = actor } } @@ -263,8 +259,13 @@ class LTS(sequentialSpecification: Class<*>) { block(old, this.computeRemappingFunction(old)) } else { val newSeqToCreate = if (curOperation != null) this.state.seqToCreate + curOperation else emptyList() - stateInfos[this] = this.also { it.state = State(newSeqToCreate) } - return block(stateInfos[this]!!, null) + val obj = this.also { it.state = State(newSeqToCreate) } + stateInfos[this] = obj + //if(stateInfos[this] != obj) { + // TODO author please check this code. Sometimes it throws + // throw RuntimeException("stateInfos[this] != obj") + //} + return block(obj, null) } } @@ -279,7 +280,7 @@ class LTS(sequentialSpecification: Class<*>) { ).intern(null) { _, _ -> initialState } } - private fun createInitialStateInstance() = sequentialSpecification.newInstance() + private fun createInitialStateInstance(): Any = sequentialSpecification.getInitialState() fun checkStateEquivalenceImplementation(): Boolean { val i1 = createInitialStateInstance() @@ -317,24 +318,24 @@ class LTS(sequentialSpecification: Class<*>) { fun generateDotGraph(): String { val builder = StringBuilder() - builder.appendln("digraph {") - builder.appendln("\"${initialState.hashCode()}\" [style=filled, fillcolor=green]") - builder.appendTransitions(initialState, IdentityHashMap()) - builder.appendln("}") + builder.appendLine("digraph {") + builder.appendLine("\"${initialState.hashCode()}\" [style=filled, fillcolor=green]") + builder.appendTransitions(initialState, HashMap()) + builder.appendLine("}") return builder.toString() } - private fun StringBuilder.appendTransitions(state: State, visitedStates: IdentityHashMap) { - state.transitionsByRequests.forEach { actor, transition -> - appendln("${state.hashCode()} -> ${transition.nextState.hashCode()} [ label=\", rf=${transition.rf?.contentToString()}\" ]") + private fun StringBuilder.appendTransitions(state: State, visitedStates: HashMap) { + state.transitionsByRequests.forEach { (actor, transition) -> + appendLine("${state.hashCode()} -> ${transition.nextState.hashCode()} [ label=\", rf=${transition.rf?.contentToString()}\" ]") if (visitedStates.put(transition.nextState, Unit) === null) appendTransitions(transition.nextState, visitedStates) } - state.transitionsByFollowUps.forEach { ticket, transition -> - appendln("${state.hashCode()} -> ${transition.nextState.hashCode()} [ label=\", rf=${transition.rf?.contentToString()}\" ]") + state.transitionsByFollowUps.forEach { (ticket, transition) -> + appendLine("${state.hashCode()} -> ${transition.nextState.hashCode()} [ label=\", rf=${transition.rf?.contentToString()}\" ]") if (visitedStates.put(transition.nextState, Unit) === null) appendTransitions(transition.nextState, visitedStates) } - state.transitionsByCancellations.forEach { ticket, transition -> - appendln("${state.hashCode()} -> ${transition.nextState.hashCode()} [ label=\", rf=${transition.rf?.contentToString()}\" ]") + state.transitionsByCancellations.forEach { (ticket, transition) -> + appendLine("${state.hashCode()} -> ${transition.nextState.hashCode()} [ label=\", rf=${transition.rf?.contentToString()}\" ]") if (visitedStates.put(transition.nextState, Unit) === null) appendTransitions(transition.nextState, visitedStates) } } @@ -362,11 +363,11 @@ private class StateInfo( resumedOperations == other.resumedOperations } - override fun hashCode() = Objects.hash( + override fun hashCode() = arrayOf( instance, suspendedOperations.map { it.actor }, resumedOperations - ) + ).contentHashCode() val maxTicket: Int get() = max( diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/SerializabilityVerifier.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/SerializabilityVerifier.kt similarity index 87% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/SerializabilityVerifier.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/verifier/SerializabilityVerifier.kt index 70a05fbb0..a2a4f17dd 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/SerializabilityVerifier.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/SerializabilityVerifier.kt @@ -1,26 +1,25 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.verifier +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* @@ -28,14 +27,14 @@ import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* * This verifier checks that the specified results could be happen in serializable execution. * It just tries to find any operations sequence which execution produces the same results. */ -public class SerializabilityVerifier( - sequentialSpecification: Class +class SerializabilityVerifier( + sequentialSpecification: SequentialSpecification<*> ) : CachedVerifier() { private val linerizabilityVerifier = LinearizabilityVerifier(sequentialSpecification) // always ignore clocks - override fun verifyResults(scenario: ExecutionScenario, results: ExecutionResult) = - super.verifyResults(scenario, results.withEmptyClocks) + override fun verifyResults(scenario: ExecutionScenario, result: ExecutionResult) = + super.verifyResults(scenario, result.withEmptyClocks) override fun verifyResultsImpl(scenario: ExecutionScenario, results: ExecutionResult) = linerizabilityVerifier.verifyResultsImpl(scenario.converted, results.converted) diff --git a/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/Verifier.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/Verifier.kt new file mode 100644 index 000000000..5c43a0b80 --- /dev/null +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/Verifier.kt @@ -0,0 +1,83 @@ +/* +* #%L +* Lincheck +* %% +* Copyright (C) 2015 - 2018 Devexperts, LLC +* %% +* 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.verifier + +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario + +/** + * Implementation of this interface verifies that execution is correct with respect to the algorithm contract. + * By default, it checks for linearizability (see [LinearizabilityVerifier]). + * + * + * IMPORTANT! + * All implementations should have `(sequentialSpecification: SequentialSpecification<*>)` constructor, + * which takes the scenario to be tested and the correct sequential implementation of the testing data structure. + */ +interface Verifier { + /** + * Verifies the specified results for correctness. + * Returns `true` if results are possible, `false` otherwise. + */ + fun verifyResults(scenario: ExecutionScenario, result: ExecutionResult): Boolean + + /** + * Returns `true` when the state equivalence relation for the sequential specification + * is properly specified via [.equals] and [.hashCode] methods. Returns + * `false` when two logically equal states do not satisfy the equals-hashCode contract. + */ + fun checkStateEquivalenceImplementation(): Boolean +} + +internal inline fun Map.computeIfAbsent(key: K, defaultValue: (K) -> V): V { + val value = get(key) + if (value == null && !containsKey(key)) { + return defaultValue(key) + } else { + @Suppress("UNCHECKED_CAST") + return value as V + } +} + +/** + * This verifier cached the already verified results in a hash table, + * and look into this hash table at first. In case of many invocations + * with the same scenario, this optimization improves the verification + * phase significantly. + */ +abstract class CachedVerifier : Verifier { + private var lastScenario: ExecutionScenario? = null + private val previousResults = HashSet() + + override fun verifyResults(scenario: ExecutionScenario, result: ExecutionResult): Boolean { + if (lastScenario != scenario) { + lastScenario = scenario + previousResults.clear() + } + val newResult = previousResults.add(result) + return if (!newResult) true else verifyResultsImpl(scenario, result) + } + + abstract fun verifyResultsImpl(scenario: ExecutionScenario, results: ExecutionResult): Boolean +} + +internal class DummySequentialSpecification private constructor() // This dummy class should not be created \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/VerifierState.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/VerifierState.kt similarity index 92% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/VerifierState.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/verifier/VerifierState.kt index b02bb7fec..5c19cf23d 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/VerifierState.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/VerifierState.kt @@ -1,23 +1,21 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.verifier diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/linearizability/LinearizabilityVerifier.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/linearizability/LinearizabilityVerifier.kt similarity index 97% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/linearizability/LinearizabilityVerifier.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/verifier/linearizability/LinearizabilityVerifier.kt index 2d3e7beb7..79d203731 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/linearizability/LinearizabilityVerifier.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/linearizability/LinearizabilityVerifier.kt @@ -24,6 +24,7 @@ package org.jetbrains.kotlinx.lincheck.verifier.linearizability import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.reflect.* /** * This verifier checks that the specified results could happen if the testing operations are linearizable. @@ -34,7 +35,7 @@ import org.jetbrains.kotlinx.lincheck.verifier.* * This verifier is based on [AbstractLTSVerifier] and caches the already processed results * for performance improvement (see [CachedVerifier]). */ -class LinearizabilityVerifier(sequentialSpecification: Class<*>) : AbstractLTSVerifier(sequentialSpecification) { +class LinearizabilityVerifier(sequentialSpecification: SequentialSpecification<*>) : AbstractLTSVerifier(sequentialSpecification) { override val lts: LTS = LTS(sequentialSpecification = sequentialSpecification) override fun createInitialContext(scenario: ExecutionScenario, results: ExecutionResult) = diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/quiescent/QuiescentConsistencyVerifier.kt b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/quiescent/QuiescentConsistencyVerifier.kt similarity index 78% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/quiescent/QuiescentConsistencyVerifier.kt rename to src/common/main/org/jetbrains/kotlinx/lincheck/verifier/quiescent/QuiescentConsistencyVerifier.kt index c755a726d..cd6d62373 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/quiescent/QuiescentConsistencyVerifier.kt +++ b/src/common/main/org/jetbrains/kotlinx/lincheck/verifier/quiescent/QuiescentConsistencyVerifier.kt @@ -25,7 +25,6 @@ import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* -import java.util.* import kotlin.collections.ArrayList /** @@ -35,35 +34,41 @@ import kotlin.collections.ArrayList * However, we believe that quiescent points do not occur * in practice while supporting them complicates the implementation. */ -class QuiescentConsistencyVerifier(sequentialSpecification: Class<*>) : Verifier { +class QuiescentConsistencyVerifier(sequentialSpecification: SequentialSpecification<*>) : Verifier { private val linearizabilityVerifier = LinearizabilityVerifier(sequentialSpecification) - private val scenarioMapping: MutableMap = WeakHashMap() + private var lastScenario: ExecutionScenario? = null + private lateinit var lastConvertedScenario: ExecutionScenario override fun checkStateEquivalenceImplementation(): Boolean = linearizabilityVerifier.checkStateEquivalenceImplementation() - override fun verifyResults(scenario: ExecutionScenario, results: ExecutionResult): Boolean { + override fun verifyResults(scenario: ExecutionScenario, result: ExecutionResult): Boolean { val convertedScenario = scenario.converted - val convertedResults = results.convert(scenario, convertedScenario.threads) + val convertedResults = result.convert(scenario, convertedScenario.threads) checkScenarioAndResultsAreSimilarlyConverted(convertedScenario, convertedResults) return linearizabilityVerifier.verifyResults(convertedScenario, convertedResults) } - private val ExecutionScenario.converted: ExecutionScenario get() = scenarioMapping.computeIfAbsent(this) { - val parallelExecutionConverted = ArrayList>() - repeat(threads) { - parallelExecutionConverted.add(ArrayList()) - } - parallelExecution.forEachIndexed { t, threadActors -> - for (a in threadActors) { - if (a.isQuiescentConsistent) { - parallelExecutionConverted.add(mutableListOf(a)) - } else { - parallelExecutionConverted[t].add(a) + private val ExecutionScenario.converted: ExecutionScenario + get() { + if (lastScenario != this) { + lastScenario = this + val parallelExecutionConverted = ArrayList>() + repeat(threads) { + parallelExecutionConverted.add(ArrayList()) } + parallelExecution.forEachIndexed { t, threadActors -> + for (a in threadActors) { + if (a.isQuiescentConsistent) { + parallelExecutionConverted.add(mutableListOf(a)) + } else { + parallelExecutionConverted[t].add(a) + } + } + } + lastConvertedScenario = ExecutionScenario(initExecution, parallelExecutionConverted, postExecution) } + return lastConvertedScenario } - ExecutionScenario(initExecution, parallelExecutionConverted, postExecution) - } private fun ExecutionResult.convert(originalScenario: ExecutionScenario, newThreads: Int): ExecutionResult { val parallelResults = ArrayList>() @@ -83,7 +88,7 @@ class QuiescentConsistencyVerifier(sequentialSpecification: Class<*>) : Verifier clockMapping[t].add(clockMapping[t][i] + 1) val c = IntArray(newThreads) { 0 } clocks[t].add(c) - parallelResults[t].add(ResultWithClock(r.result, HBClock(c))) + parallelResults[t].add(ResultWithClock(r.result, HBClock(c.toLincheckAtomicIntArray()))) } } } @@ -116,8 +121,6 @@ class QuiescentConsistencyVerifier(sequentialSpecification: Class<*>) : Verifier } } -private val Actor.isQuiescentConsistent: Boolean get() = method.isAnnotationPresent(QuiescentConsistent::class.java) - /** * This annotation indicates that the method it is presented on * is quiescent consistent. diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Actor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Actor.kt index 208a3fd5d..5abf2b0cf 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Actor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Actor.kt @@ -22,8 +22,10 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.annotations.* -import java.lang.reflect.Method -import kotlin.reflect.jvm.* +import org.jetbrains.kotlinx.lincheck.verifier.quiescent.* +import java.lang.reflect.* +import kotlin.reflect.KClass +import kotlin.reflect.jvm.kotlinFunction /** * The actor entity describe the operation with its parameters @@ -31,32 +33,39 @@ import kotlin.reflect.jvm.* * * @see Operation */ -data class Actor @JvmOverloads constructor( +actual data class Actor @JvmOverloads constructor( val method: Method, val arguments: List, - val handledExceptions: List> = emptyList(), - val cancelOnSuspension: Boolean = false, - val allowExtraSuspension: Boolean = false, + actual val handledExceptions: List> = emptyList(), + actual val cancelOnSuspension: Boolean = false, + actual val allowExtraSuspension: Boolean = false, val blocking: Boolean = false, val causesBlocking: Boolean = false, - val promptCancellation: Boolean = false, + actual val promptCancellation: Boolean = false, // we have to specify `isSuspendable` property explicitly for transformed classes since // `isSuspendable` implementation produces a circular dependency and, therefore, fails. - val isSuspendable: Boolean = method.isSuspendable() + actual val isSuspendable: Boolean = method.isSuspendable() ) { + init { if (promptCancellation) require(cancelOnSuspension) { "`promptCancellation` cannot be set to `true` if `cancelOnSuspension` is `false`" } } - override fun toString() = method.name + + actual override fun toString() = method.name + arguments.joinToString(prefix = "(", postfix = ")", separator = ", ") { it.toString() } + (if (cancelOnSuspension) " + " else "") + (if (promptCancellation) "prompt_" else "") + (if (cancelOnSuspension) "cancel" else "") + actual fun finalize() { + // do nothing + } + val handlesExceptions = handledExceptions.isNotEmpty() } -fun Method.isSuspendable(): Boolean = kotlinFunction?.isSuspend ?: false \ No newline at end of file +fun Method.isSuspendable(): Boolean = kotlinFunction?.isSuspend ?: false + +actual val Actor.isQuiescentConsistent: Boolean get() = method.isAnnotationPresent(QuiescentConsistent::class.java) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt index 9676ab6dc..979e12da6 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestConfiguration.kt @@ -23,7 +23,6 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.CTestConfiguration.Companion.DEFAULT_TIMEOUT_MS import org.jetbrains.kotlinx.lincheck.execution.* -import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedCTestConfiguration.Companion.DEFAULT_ELIMINATE_LOCAL_OBJECTS import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedCTestConfiguration.Companion.DEFAULT_VERBOSE_TRACE @@ -31,59 +30,35 @@ import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* import org.jetbrains.kotlinx.lincheck.strategy.stress.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.jetbrains.kotlinx.lincheck.verifier.linearizability.* -import java.lang.reflect.* - -/** - * Abstract configuration for different lincheck modes. - */ -abstract class CTestConfiguration( - val testClass: Class<*>, - val iterations: Int, - val threads: Int, - val actorsPerThread: Int, - val actorsBefore: Int, - val actorsAfter: Int, - val generatorClass: Class, - val verifierClass: Class, - val requireStateEquivalenceImplCheck: Boolean, - val minimizeFailedScenario: Boolean, - val sequentialSpecification: Class<*>, - val timeoutMs: Long, - val customScenarios: List -) { - abstract fun createStrategy(testClass: Class<*>, scenario: ExecutionScenario, validationFunctions: List, - stateRepresentationMethod: Method?, verifier: Verifier): Strategy - companion object { - const val DEFAULT_ITERATIONS = 100 - const val DEFAULT_THREADS = 2 - const val DEFAULT_ACTORS_PER_THREAD = 5 - const val DEFAULT_ACTORS_BEFORE = 5 - const val DEFAULT_ACTORS_AFTER = 5 - val DEFAULT_EXECUTION_GENERATOR: Class = RandomExecutionGenerator::class.java - val DEFAULT_VERIFIER: Class = LinearizabilityVerifier::class.java - const val DEFAULT_MINIMIZE_ERROR = true - const val DEFAULT_TIMEOUT_MS: Long = 10000 - } -} +import kotlin.reflect.* internal fun createFromTestClassAnnotations(testClass: Class<*>): List { val stressConfigurations: List = testClass.getAnnotationsByType(StressCTest::class.java) .map { ann: StressCTest -> - StressCTestConfiguration(testClass, ann.iterations, + StressCTestConfiguration(TestClass(testClass), ann.iterations, ann.threads, ann.actorsPerThread, ann.actorsBefore, ann.actorsAfter, - ann.generator.java, ann.verifier.java, ann.invocationsPerIteration, + { testConfiguration, testStructure -> + ann.generator.java.getConstructor(CTestConfiguration::class.java, CTestStructure::class.java) + .newInstance(testConfiguration, testStructure) + }, { sequentialSpecification -> + ann.verifier.java.getConstructor(SequentialSpecification::class.java).newInstance(sequentialSpecification) + }, ann.invocationsPerIteration, ann.requireStateEquivalenceImplCheck, ann.minimizeFailedScenario, - chooseSequentialSpecification(ann.sequentialSpecification.java, testClass), - DEFAULT_TIMEOUT_MS, emptyList() + chooseSequentialSpecification(ann.sequentialSpecification.java, TestClass(testClass)), DEFAULT_TIMEOUT_MS, emptyList() ) } val modelCheckingConfigurations: List = testClass.getAnnotationsByType(ModelCheckingCTest::class.java) .map { ann: ModelCheckingCTest -> - ModelCheckingCTestConfiguration(testClass, ann.iterations, + ModelCheckingCTestConfiguration(TestClass(testClass), ann.iterations, ann.threads, ann.actorsPerThread, ann.actorsBefore, ann.actorsAfter, - ann.generator.java, ann.verifier.java, ann.checkObstructionFreedom, ann.hangingDetectionThreshold, + { testConfiguration, testStructure -> + ann.generator.java.getConstructor(CTestConfiguration::class.java, CTestStructure::class.java) + .newInstance(testConfiguration, testStructure) + }, { sequentialSpecification -> + ann.verifier.java.getConstructor(SequentialSpecification::class.java).newInstance(sequentialSpecification) + }, ann.checkObstructionFreedom, ann.hangingDetectionThreshold, ann.invocationsPerIteration, ManagedCTestConfiguration.DEFAULT_GUARANTEES, ann.requireStateEquivalenceImplCheck, - ann.minimizeFailedScenario, chooseSequentialSpecification(ann.sequentialSpecification.java, testClass), + ann.minimizeFailedScenario, chooseSequentialSpecification(ann.sequentialSpecification.java, TestClass(testClass)), DEFAULT_TIMEOUT_MS, DEFAULT_ELIMINATE_LOCAL_OBJECTS, DEFAULT_VERBOSE_TRACE, emptyList() ) } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestStructure.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestStructure.java deleted file mode 100644 index 4e4b95b51..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestStructure.java +++ /dev/null @@ -1,246 +0,0 @@ -package org.jetbrains.kotlinx.lincheck; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import org.jetbrains.kotlinx.lincheck.annotations.*; -import org.jetbrains.kotlinx.lincheck.execution.*; -import org.jetbrains.kotlinx.lincheck.paramgen.*; -import org.jetbrains.kotlinx.lincheck.strategy.stress.*; - -import java.lang.reflect.*; -import java.util.*; -import java.util.stream.*; - -import static org.jetbrains.kotlinx.lincheck.ActorKt.*; - -/** - * Contains information about the provided operations (see {@link Operation}). - * Several {@link StressCTest tests} can refer to one structure - * (i.e. one test class could have several {@link StressCTest} annotations) - */ -public class CTestStructure { - public final List actorGenerators; - public final List operationGroups; - public final List validationFunctions; - public final Method stateRepresentation; - - private CTestStructure(List actorGenerators, List operationGroups, - List validationFunctions, Method stateRepresentation) { - this.actorGenerators = actorGenerators; - this.operationGroups = operationGroups; - this.validationFunctions = validationFunctions; - this.stateRepresentation = stateRepresentation; - } - - /** - * Constructs {@link CTestStructure} for the specified test class. - */ - public static CTestStructure getFromTestClass(Class testClass) { - Map> namedGens = new HashMap<>(); - Map groupConfigs = new HashMap<>(); - List actorGenerators = new ArrayList<>(); - List validationFunctions = new ArrayList<>(); - List stateRepresentations = new ArrayList<>(); - Class clazz = testClass; - while (clazz != null) { - readTestStructureFromClass(clazz, namedGens, groupConfigs, actorGenerators, validationFunctions, stateRepresentations); - 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 - return new CTestStructure(actorGenerators, new ArrayList<>(groupConfigs.values()), validationFunctions, stateRepresentation); - } - - private static void readTestStructureFromClass(Class clazz, Map> namedGens, - Map groupConfigs, - List actorGenerators, - List validationFunctions, - List stateRepresentations) { - // Read named parameter paramgen (declared for class) - for (Param paramAnn : clazz.getAnnotationsByType(Param.class)) { - if (paramAnn.name().isEmpty()) { - throw new IllegalArgumentException("@Param name in class declaration cannot be empty"); - } - namedGens.put(paramAnn.name(), createGenerator(paramAnn)); - } - // Create map for default (not named) gens - Map, ParameterGenerator> defaultGens = createDefaultGenerators(); - // Read group configurations - for (OpGroupConfig opGroupConfigAnn: clazz.getAnnotationsByType(OpGroupConfig.class)) { - groupConfigs.put(opGroupConfigAnn.name(), new OperationGroup(opGroupConfigAnn.name(), - opGroupConfigAnn.nonParallel())); - } - // Create actor paramgen - for (Method m : getDeclaredMethodSorted(clazz)) { - // Operation - if (m.isAnnotationPresent(Operation.class)) { - Operation opAnn = m.getAnnotation(Operation.class); - boolean isSuspendableMethod = isSuspendable(m); - // Check that params() in @Operation is empty or has the same size as the method - if (opAnn.params().length > 0 && opAnn.params().length != m.getParameterCount()) { - throw new IllegalArgumentException("Invalid count of paramgen for " + m.toString() - + " method in @Operation"); - } - // Construct list of parameter paramgen - final List> gens = new ArrayList<>(); - int nParameters = m.getParameterCount() - (isSuspendableMethod ? 1 : 0); - for (int i = 0; i < nParameters; i++) { - String nameInOperation = opAnn.params().length > 0 ? opAnn.params()[i] : null; - gens.add(getOrCreateGenerator(m, m.getParameters()[i], nameInOperation, namedGens, defaultGens)); - } - // Get list of handled exceptions if they are presented - List> handledExceptions = Arrays.asList(opAnn.handleExceptionsAsResult()); - ActorGenerator actorGenerator = new ActorGenerator(m, gens, handledExceptions, opAnn.runOnce(), - opAnn.cancellableOnSuspension(), opAnn.allowExtraSuspension(), opAnn.blocking(), opAnn.causesBlocking(), - opAnn.promptCancellation()); - 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); - if (operationGroup == null) - throw new IllegalStateException("Operation group " + opGroup + " is not configured"); - operationGroup.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(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); - } - } - } - - /** - * Sort methods by name to make scenario generation deterministic. - */ - private static Method[] getDeclaredMethodSorted(Class clazz) { - Method[] methods = clazz.getDeclaredMethods(); - Comparator comparator = Comparator - // compare by method name - .comparing(Method::getName) - // then compare by parameter class names - .thenComparing(m -> Arrays.stream(m.getParameterTypes()).map(Class::getName).collect(Collectors.joining(":"))); - Arrays.sort(methods, comparator); - return methods; - } - - private static ParameterGenerator getOrCreateGenerator(Method m, Parameter p, String nameInOperation, - Map> namedGens, Map, ParameterGenerator> defaultGens) - { - // Read @Param annotation on the parameter - Param paramAnn = p.getAnnotation(Param.class); - // If this annotation not presented use named generator based on name presented in @Operation or parameter name. - if (paramAnn == null) { - // If name in @Operation is presented, return the generator with this name, - // otherwise return generator with parameter's name - String name = nameInOperation != null ? nameInOperation : - (p.isNamePresent() ? p.getName() : null); - if (name != null) - return checkAndGetNamedGenerator(namedGens, name); - // Parameter generator is not specified, try to create a default one - ParameterGenerator defaultGenerator = defaultGens.get(p.getType()); - if (defaultGenerator != null) - return defaultGenerator; - // Cannot create default parameter generator, throw an exception - throw new IllegalStateException("Generator for parameter \"" + p + "\" in method \"" - + m.getName() + "\" should be specified."); - } - // If the @Param annotation is presented check it's correctness firstly - if (!paramAnn.name().isEmpty() && !(paramAnn.gen() == ParameterGenerator.Dummy.class)) - throw new IllegalStateException("@Param should have either name or gen with optionally configuration"); - // If @Param annotation specifies generator's name then return the specified generator - if (!paramAnn.name().isEmpty()) - return checkAndGetNamedGenerator(namedGens, paramAnn.name()); - // Otherwise create new parameter generator - return createGenerator(paramAnn); - } - - private static ParameterGenerator createGenerator(Param paramAnn) { - try { - return paramAnn.gen().getConstructor(String.class).newInstance(paramAnn.conf()); - } catch (Exception e) { - throw new IllegalStateException("Cannot create parameter gen", e); - } - } - - private static Map, ParameterGenerator> createDefaultGenerators() { - Map, ParameterGenerator> defaultGens = new HashMap<>(); - defaultGens.put(boolean.class, new BooleanGen("")); - defaultGens.put(Boolean.class, defaultGens.get(boolean.class)); - defaultGens.put(byte.class, new ByteGen("")); - defaultGens.put(Byte.class, defaultGens.get(byte.class)); - defaultGens.put(short.class, new ShortGen("")); - defaultGens.put(Short.class, defaultGens.get(short.class)); - defaultGens.put(int.class, new IntGen("")); - defaultGens.put(Integer.class, defaultGens.get(int.class)); - defaultGens.put(long.class, new LongGen("")); - defaultGens.put(Long.class, defaultGens.get(long.class)); - defaultGens.put(float.class, new FloatGen("")); - defaultGens.put(Float.class, defaultGens.get(float.class)); - defaultGens.put(double.class, new DoubleGen("")); - defaultGens.put(Double.class, defaultGens.get(double.class)); - defaultGens.put(String.class, new StringGen("")); - return defaultGens; - } - - private static ParameterGenerator checkAndGetNamedGenerator(Map> namedGens, String name) { - return Objects.requireNonNull(namedGens.get(name), "Unknown generator name: \"" + name + "\""); - } - - public static class OperationGroup { - public final String name; - public final boolean nonParallel; - public final List actors; - - public OperationGroup(String name, boolean nonParallel) { - this.name = name; - this.nonParallel = nonParallel; - this.actors = new ArrayList<>(); - } - - @Override - public String toString() { - return "OperationGroup{" + - "name='" + name + '\'' + - ", nonParallel=" + nonParallel + - ", actors=" + actors + - '}'; - } - } -} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestStructure.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestStructure.kt new file mode 100644 index 000000000..84387dcb2 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CTestStructure.kt @@ -0,0 +1,207 @@ +/* + * 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.annotations.* +import org.jetbrains.kotlinx.lincheck.execution.ActorGenerator +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.paramgen.ParameterGenerator.Dummy +import java.lang.Exception +import java.lang.reflect.Method +import java.lang.reflect.Parameter +import java.util.* +import java.util.stream.Collectors +import kotlin.collections.HashMap +import kotlin.reflect.KClass + +actual typealias ValidationFunction = Method + +actual val ValidationFunction.name get() = this.name + +actual typealias StateRepresentationFunction = Method + +/** + * Contains information about the provided operations (see [Operation]). + * Several [tests][StressCTest] can refer to one structure + * (i.e. one test class could have several [StressCTest] annotations) + */ +actual class CTestStructure private constructor( + actual val actorGenerators: List, + actual val operationGroups: List, + actual val validationFunctions: List, + actual val stateRepresentation: StateRepresentationFunction? +) { + companion object { + /** + * Constructs [CTestStructure] for the specified test class. + */ + fun getFromTestClass(testClass: Class<*>?): CTestStructure { + val namedGens: MutableMap> = HashMap() + val groupConfigs: MutableMap = HashMap() + val actorGenerators: MutableList = ArrayList() + val validationFunctions: MutableList = ArrayList() + val stateRepresentations: MutableList = ArrayList() + var clazz = testClass + while (clazz != null) { + readTestStructureFromClass(clazz, namedGens, groupConfigs, actorGenerators, validationFunctions, stateRepresentations) + clazz = clazz.superclass + } + check(stateRepresentations.size <= 1) { + "Several functions marked with " + StateRepresentation::class.java.simpleName + + " were found, while at most one should be specified: " + + stateRepresentations.stream().map { obj: StateRepresentationFunction -> obj.name }.collect(Collectors.joining(", ")) + } + var stateRepresentation: StateRepresentationFunction? = null + if (stateRepresentations.isNotEmpty()) stateRepresentation = stateRepresentations[0] + // Create StressCTest class configuration + return CTestStructure(actorGenerators, ArrayList(groupConfigs.values), validationFunctions, stateRepresentation) + } + + private fun readTestStructureFromClass(clazz: Class<*>, namedGens: MutableMap>, + groupConfigs: MutableMap, + actorGenerators: MutableList, + validationFunctions: MutableList, + stateRepresentations: MutableList) { + // Read named parameter paramgen (declared for class) + for (paramAnn in clazz.getAnnotationsByType(Param::class.java)) { + require(!paramAnn.name.isEmpty()) { "@Param name in class declaration cannot be empty" } + namedGens[paramAnn.name] = createGenerator(paramAnn) + } + // Create map for default (not named) gens + val defaultGens = createDefaultGenerators() + // Read group configurations + for (opGroupConfigAnn in clazz.getAnnotationsByType(OpGroupConfig::class.java)) { + groupConfigs[opGroupConfigAnn.name] = OperationGroup(opGroupConfigAnn.name, + opGroupConfigAnn.nonParallel) + } + // Create actor paramgen + for (m in getDeclaredMethodSorted(clazz)) { + // Operation + if (m.isAnnotationPresent(Operation::class.java)) { + val opAnn = m.getAnnotation(Operation::class.java) + val isSuspendableMethod = m.isSuspendable() + // Check that params() in @Operation is empty or has the same size as the method + require(!(opAnn.params.isNotEmpty() && opAnn.params.size != m.parameterCount)) { + ("Invalid count of paramgen for " + m.toString() + + " method in @Operation") + } + // Construct list of parameter paramgen + val gens: MutableList> = ArrayList() + val nParameters = m.parameterCount - if (isSuspendableMethod) 1 else 0 + for (i in 0 until nParameters) { + val nameInOperation = if (opAnn.params.isNotEmpty()) opAnn.params[i] else null + gens.add(getOrCreateGenerator(m, m.parameters[i], nameInOperation, namedGens, defaultGens)) + } + // Get list of handled exceptions if they are presented + val handledExceptions: List> = opAnn.handleExceptionsAsResult.toList() + val actorGenerator = ActorGenerator(m, gens, handledExceptions, opAnn.runOnce, + opAnn.cancellableOnSuspension, opAnn.allowExtraSuspension, opAnn.blocking, opAnn.causesBlocking, + opAnn.promptCancellation) + actorGenerators.add(actorGenerator) + // Get list of groups and add this operation to specified ones + val opGroup = opAnn.group + if (opGroup.isNotEmpty()) { + val operationGroup = groupConfigs[opGroup] + ?: throw IllegalStateException("Operation group $opGroup is not configured") + operationGroup.actors.add(actorGenerator) + } + } + if (m.isAnnotationPresent(Validate::class.java)) { + check(m.parameterCount == 0) { "Validation function " + m.name + " should not have parameters" } + validationFunctions.add(m) + } + if (m.isAnnotationPresent(StateRepresentation::class.java)) { + check(m.parameterCount == 0) { "State representation function " + m.name + " should not have parameters" } + check(m.returnType == String::class.java) { "State representation function " + m.name + " should have String return type" } + stateRepresentations.add(m) + } + } + } + + /** + * Sort methods by name to make scenario generation deterministic. + */ + private fun getDeclaredMethodSorted(clazz: Class<*>): Array { + val methods = clazz.declaredMethods + val comparator = Comparator // compare by method name + .comparing { obj: Method -> obj.name } // then compare by parameter class names + .thenComparing { m: Method -> Arrays.stream(m.parameterTypes).map { obj: Class<*> -> obj.name }.collect(Collectors.joining(":")) } + Arrays.sort(methods, comparator) + return methods + } + + private fun getOrCreateGenerator(m: Method, p: Parameter, nameInOperation: String?, + namedGens: Map>, defaultGens: Map?, ParameterGenerator<*>>): ParameterGenerator<*> { + // Read @Param annotation on the parameter + val paramAnn = p.getAnnotation(Param::class.java) + // If this annotation not presented use named generator based on name presented in @Operation or parameter name. + if (paramAnn == null) { + // If name in @Operation is presented, return the generator with this name, + // otherwise return generator with parameter's name + val name = nameInOperation ?: if (p.isNamePresent) p.name else null + if (name != null) return checkAndGetNamedGenerator(namedGens, name) + // Parameter generator is not specified, try to create a default one + val parameterType = p.type + val defaultGenerator = defaultGens[parameterType] + if (defaultGenerator != null) return defaultGenerator + throw IllegalStateException("Generator for parameter \"" + p + "\" in method \"" + + m.name + "\" should be specified.") + } + // If the @Param annotation is presented check it's correctness firstly + check(!(!paramAnn.name.isEmpty() && paramAnn.gen != Dummy::class)) { "@Param should have either name or gen with optionally configuration" } + // If @Param annotation specifies generator's name then return the specified generator + return if (!paramAnn.name.isEmpty()) checkAndGetNamedGenerator(namedGens, paramAnn.name) else createGenerator(paramAnn) + // Otherwise create new parameter generator + } + + private fun createGenerator(paramAnn: Param): ParameterGenerator<*> { + return try { + paramAnn.gen.java.getConstructor(String::class.java).newInstance(paramAnn.conf) + } catch (e: Exception) { + throw IllegalStateException("Cannot create parameter gen", e) + } + } + + private fun createDefaultGenerators(): Map?, ParameterGenerator<*>> { + val defaultGens: MutableMap?, ParameterGenerator<*>> = HashMap() + defaultGens[Boolean::class.javaPrimitiveType] = BooleanGen("") + defaultGens[Boolean::class.javaObjectType] = defaultGens[Boolean::class.javaPrimitiveType]!! + defaultGens[Byte::class.javaPrimitiveType] = ByteGen("") + defaultGens[Byte::class.javaObjectType] = defaultGens[Byte::class.javaPrimitiveType]!! + defaultGens[Short::class.javaPrimitiveType] = ShortGen("") + defaultGens[Short::class.javaObjectType] = defaultGens[Short::class.javaPrimitiveType]!! + defaultGens[Int::class.javaPrimitiveType] = IntGen("") + defaultGens[Int::class.javaObjectType] = defaultGens[Int::class.javaPrimitiveType]!! + defaultGens[Long::class.javaPrimitiveType] = LongGen("") + defaultGens[Long::class.javaObjectType] = defaultGens[Long::class.javaPrimitiveType]!! + defaultGens[Float::class.javaPrimitiveType] = FloatGen("") + defaultGens[Float::class.javaObjectType] = defaultGens[Float::class.javaPrimitiveType]!! + defaultGens[Double::class.javaPrimitiveType] = DoubleGen("") + defaultGens[Double::class.javaObjectType] = defaultGens[Double::class.javaPrimitiveType]!! + defaultGens[String::class.javaObjectType] = StringGen("") + return defaultGens + } + + private fun checkAndGetNamedGenerator(namedGens: Map>, name: String): ParameterGenerator<*> { + return Objects.requireNonNull(namedGens[name], "Unknown generator name: \"$name\"")!! + } + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CancellabilitySupportTransformer.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CancellabilitySupportTransformer.kt index 17e7259f9..9e0d48d15 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CancellabilitySupportTransformer.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CancellabilitySupportTransformer.kt @@ -47,5 +47,5 @@ private class CancellabilitySupportMethodTransformer(access: Int, methodName: St } } -private val storeCancellableContMethod = Method.getMethod(::storeCancellableContinuation.javaMethod) +private val storeCancellableContMethod = org.objectweb.asm.commons.Method.getMethod(::storeCancellableContinuation.javaMethod) private val storeCancellableContOwnerType = Type.getType(::storeCancellableContinuation.javaMethod!!.declaringClass) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CustomScenarioDSL.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CustomScenarioDSL.kt index 318e59f06..e9a12c889 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/CustomScenarioDSL.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/CustomScenarioDSL.kt @@ -25,28 +25,7 @@ import java.lang.IllegalStateException import kotlin.reflect.KFunction import kotlin.reflect.jvm.javaMethod -/** - * Creates a custom scenario using the specified DSL as in the following example: - * - * ``` - * scenario { - * initial { - * actor(QueueTest::offer, 1) - * actor(QueueTest::offer, 2) - * } - * parallel { - * thread { - * actor(QueueTest::poll) - * } - * thread { - * actor(QueueTest::poll) - * } - * } - * } - * ``` - */ -fun scenario(block: DSLScenarioBuilder.() -> Unit): ExecutionScenario = - DSLScenarioBuilder().apply(block).buildScenario() +actual typealias ScenarioBuilder = DSLScenarioBuilder /** * Create an actor with the specified [function][f] and [arguments][args]. @@ -55,10 +34,10 @@ internal fun actor(f: KFunction<*>, vararg args: Any?, cancelOnSuspension: Boole val method = f.javaMethod ?: throw IllegalStateException("The function is a constructor or cannot be represented by a Java Method") require(method.exceptionTypes.all { Throwable::class.java.isAssignableFrom(it) }) { "Not all declared exceptions are Throwable" } return Actor( - method = method, - arguments = args.toList(), - handledExceptions = (method.exceptionTypes as Array>).toList(), - cancelOnSuspension = cancelOnSuspension + method = method, + arguments = args.toList(), + handledExceptions = (method.exceptionTypes as Array>).toList().map{it.kotlin}, + cancelOnSuspension = cancelOnSuspension ) } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.java index 720c9db87..3e8e61192 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.java +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ExecutionClassLoader.java @@ -1,5 +1,3 @@ -package org.jetbrains.kotlinx.lincheck; - /* * #%L * Lincheck @@ -22,6 +20,8 @@ * #L% */ +package org.jetbrains.kotlinx.lincheck; + import org.jetbrains.kotlinx.lincheck.runner.TestThreadExecution; /** diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt index 229dfe3be..65dd011ce 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt @@ -38,7 +38,7 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) { init { val logLevel = options?.logLevel ?: testClass.getAnnotation(LogLevel::class.java)?.value ?: DEFAULT_LOG_LEVEL reporter = Reporter(logLevel) - testConfigurations = if (options != null) listOf(options.createTestConfigurations(testClass)) + testConfigurations = if (options != null) listOf(options.createTestConfigurations(TestClass(testClass))) else createFromTestClassAnnotations(testClass) } @@ -122,12 +122,16 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) { } private fun ExecutionScenario.tryMinimize(threadId: Int, position: Int, testCfg: CTestConfiguration): LincheckFailure? { - val newScenario = this.copy() + var newScenario = this.copy() val actors = newScenario[threadId] as MutableList actors.removeAt(position) if (actors.isEmpty() && threadId != 0 && threadId != newScenario.threads + 1) { // Also remove the empty thread - newScenario.parallelExecution.removeAt(threadId - 1) + newScenario = ExecutionScenario( + newScenario.initExecution, + newScenario.parallelExecution.filterIndexed{id, _ -> id != threadId - 1}, + newScenario.postExecution + ) } return if (newScenario.isValid) { val verifier = testCfg.createVerifier(checkStateEquivalence = false) @@ -137,10 +141,10 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) { private fun ExecutionScenario.run(testCfg: CTestConfiguration, verifier: Verifier): LincheckFailure? = testCfg.createStrategy( - testClass = testClass, + testClass = TestClass(testClass), scenario = this, validationFunctions = testStructure.validationFunctions, - stateRepresentationMethod = testStructure.stateRepresentation, + stateRepresentationFunction = testStructure.stateRepresentation, verifier = verifier ).run() @@ -169,15 +173,15 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) { } private val ExecutionScenario.hasSuspendableActorsInInitPart get() = - initExecution.stream().anyMatch(Actor::isSuspendable) + initExecution.any(Actor::isSuspendable) private val ExecutionScenario.hasPostPartAndSuspendableActors get() = - (parallelExecution.stream().anyMatch { actors -> actors.stream().anyMatch { it.isSuspendable } } && postExecution.size > 0) + (parallelExecution.any { actors -> actors.any { it.isSuspendable } } && postExecution.size > 0) private val ExecutionScenario.isParallelPartEmpty get() = parallelExecution.map { it.size }.sum() == 0 private fun CTestConfiguration.createVerifier(checkStateEquivalence: Boolean) = - verifierClass.getConstructor(Class::class.java).newInstance(sequentialSpecification).also { + verifierGenerator(this.sequentialSpecification).also { if (!checkStateEquivalence) return@also val stateEquivalenceCorrect = it.checkStateEquivalenceImplementation() if (!stateEquivalenceCorrect) { @@ -191,10 +195,7 @@ class LinChecker (private val testClass: Class<*>, options: Options<*, *>?) { } private fun CTestConfiguration.createExecutionGenerator() = - generatorClass.getConstructor( - CTestConfiguration::class.java, - CTestStructure::class.java - ).newInstance(this, testStructure) + executionGenerator(this, testStructure) // This companion object is used for backwards compatibility. companion object { diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Options.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Options.kt index be81269ab..728e4f630 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Options.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Options.kt @@ -1,182 +1,41 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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 -import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.verifier.* -/** - * Abstract class for test options. - */ -abstract class Options, CTEST : CTestConfiguration> { - internal var logLevel = DEFAULT_LOG_LEVEL - protected var iterations = CTestConfiguration.DEFAULT_ITERATIONS - protected var threads = CTestConfiguration.DEFAULT_THREADS - protected var actorsPerThread = CTestConfiguration.DEFAULT_ACTORS_PER_THREAD - protected var actorsBefore = CTestConfiguration.DEFAULT_ACTORS_BEFORE - protected var actorsAfter = CTestConfiguration.DEFAULT_ACTORS_AFTER - protected var executionGenerator = CTestConfiguration.DEFAULT_EXECUTION_GENERATOR - protected var verifier = CTestConfiguration.DEFAULT_VERIFIER - protected var requireStateEquivalenceImplementationCheck = false - protected var minimizeFailedScenario = CTestConfiguration.DEFAULT_MINIMIZE_ERROR - protected var sequentialSpecification: Class<*>? = null - protected var timeoutMs: Long = CTestConfiguration.DEFAULT_TIMEOUT_MS - protected var customScenarios: MutableList = mutableListOf() - - /** - * Number of different test scenarios to be executed - */ - fun iterations(iterations: Int): OPT = applyAndCast { - this.iterations = iterations - } - - /** - * Use the specified number of threads for the parallel part of an execution. - * - * Note, that the the actual number of threads can be less due to some restrictions - * like [Operation.runOnce]. - * - * @see ExecutionScenario.parallelExecution - */ - fun threads(threads: Int): OPT = applyAndCast { - this.threads = threads - } - - /** - * Generate the specified number of operations for each thread of the parallel part of an execution. - * - * Note, that the the actual number of operations can be less due to some restrictions - * like [Operation.runOnce]. - * - * @see ExecutionScenario.parallelExecution - */ - fun actorsPerThread(actorsPerThread: Int): OPT = applyAndCast { - this.actorsPerThread = actorsPerThread - } - - /** - * Generate the specified number of operation for the initial sequential part of an execution. - * - * Note, that the the actual number of operations can be less due to some restrictions - * like [Operation.runOnce]. - * - * @see ExecutionScenario.initExecution - */ - fun actorsBefore(actorsBefore: Int): OPT = applyAndCast { - this.actorsBefore = actorsBefore - } - - /** - * Generate the specified number of operation for the last sequential part of an execution. - * - * Note, that the the actual number of operations can be less due to some restrictions - * like [Operation.runOnce]. - * - * @see ExecutionScenario.postExecution - */ - fun actorsAfter(actorsAfter: Int): OPT = applyAndCast { - this.actorsAfter = actorsAfter - } - - /** - * Use the specified execution generator. - */ - fun executionGenerator(executionGenerator: Class): OPT = applyAndCast { - this.executionGenerator = executionGenerator - } - - /** - * Use the specified verifier. - */ - fun verifier(verifier: Class): OPT = applyAndCast { - this.verifier = verifier - } +actual typealias SequentialSpecification = Class +actual fun SequentialSpecification.getInitialState(): T = this.getDeclaredConstructor().newInstance() - /** - * Require correctness check of test instance state equivalency relation defined by the user. - * It checks whether two new instances of a test class are equal. - * If the check fails [[IllegalStateException]] is thrown. - */ - fun requireStateEquivalenceImplCheck(require: Boolean): OPT = applyAndCast { - requireStateEquivalenceImplementationCheck = require - } - - /** - * If this feature is enabled and an invalid interleaving has been found, - * *lincheck* tries to minimize the corresponding scenario in order to - * construct a smaller one so that the test fails on it as well. - * Enabled by default. - */ - fun minimizeFailedScenario(minimizeFailedScenario: Boolean): OPT = applyAndCast { - this.minimizeFailedScenario = minimizeFailedScenario - } - - abstract fun createTestConfigurations(testClass: Class<*>): CTEST - - /** - * Set logging level, [DEFAULT_LOG_LEVEL] is used by default. - */ - fun logLevel(logLevel: LoggingLevel): OPT = applyAndCast { - this.logLevel = logLevel - } - - /** - * The specified class defines the sequential behavior of the testing data structure; - * it is used by [Verifier] to build a labeled transition system, - * and should have the same methods as the testing data structure. - * - * By default, the provided concurrent implementation is used in a sequential way. - */ - fun sequentialSpecification(clazz: Class<*>?): OPT = applyAndCast { - sequentialSpecification = clazz - } - - /** - * Examine the specified custom scenario additionally to the generated ones. - */ - fun addCustomScenario(scenario: ExecutionScenario) = applyAndCast { - customScenarios.add(scenario) - } - - /** - * Examine the specified custom scenario additionally to the generated ones. - */ - fun addCustomScenario(scenarioBuilder: DSLScenarioBuilder.() -> Unit) = - addCustomScenario(scenario { scenarioBuilder() }) - - /** - * Internal, DO NOT USE. - */ - internal fun invocationTimeout(timeoutMs: Long): OPT = applyAndCast { - this.timeoutMs = timeoutMs +fun , CTEST : CTestConfiguration> Options.executionGenerator(executionGeneratorClass: Class): OPT { + executionGenerator { + testConfiguration, testStructure -> executionGeneratorClass.getConstructor(CTestConfiguration::class.java, CTestStructure::class.java) + .newInstance(testConfiguration, testStructure) } + return this as OPT +} - companion object { - @Suppress("UNCHECKED_CAST") - private inline fun , CTEST : CTestConfiguration> Options.applyAndCast( - block: Options.() -> Unit - ) = this.apply { - block() - } as OPT +fun , CTEST : CTestConfiguration> Options.verifier(verifierClass: Class): OPT { + verifier { sequentialSpecification -> + verifierClass.getConstructor(SequentialSpecification::class.java).newInstance(sequentialSpecification) } -} + return this as OPT +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt index 1098a4953..fc1363dfa 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt @@ -1,156 +1,35 @@ /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * Copyright (C) 2019 - 2021 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 -import org.jetbrains.kotlinx.lincheck.LoggingLevel.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* -import java.io.* -class Reporter constructor(val logLevel: LoggingLevel) { - private val out: PrintStream = System.out - private val outErr: PrintStream = System.err - - fun logIteration(iteration: Int, maxIterations: Int, scenario: ExecutionScenario) = log(INFO) { - appendln("\n= Iteration $iteration / $maxIterations =") - appendExecutionScenario(scenario) - } - - fun logFailedIteration(failure: LincheckFailure) = log(INFO) { - appendFailure(failure) - } - - fun logScenarioMinimization(scenario: ExecutionScenario) = log(INFO) { - appendln("\nInvalid interleaving found, trying to minimize the scenario below:") - appendExecutionScenario(scenario) - } - - fun logStateEquivalenceViolation(sequentialSpecification: Class<*>) = log(WARN) { - appendStateEquivalenceViolationMessage(sequentialSpecification) - } - - private inline fun log(logLevel: LoggingLevel, crossinline msg: StringBuilder.() -> Unit): Unit = synchronized(this) { - if (this.logLevel > logLevel) return - val sb = StringBuilder() - msg(sb) - val output = if (logLevel == WARN) outErr else out - output.println(sb) - } -} - -@JvmField val DEFAULT_LOG_LEVEL = WARN -enum class LoggingLevel { - INFO, WARN -} - -internal fun printInColumnsCustom( - groupedObjects: List>, - joinColumns: (List) -> String -): String { - val nRows = groupedObjects.map { it.size }.max() ?: 0 - val nColumns = groupedObjects.size - val rows = (0 until nRows).map { rowIndex -> - (0 until nColumns) - .map { groupedObjects[it] } - .map { it.getOrNull(rowIndex)?.toString().orEmpty() } // print empty strings for empty cells - } - val columnWidths: List = (0 until nColumns).map { columnIndex -> - (0 until nRows).map { rowIndex -> rows[rowIndex][columnIndex].length }.max() ?: 0 - } - return (0 until nRows) - .map { rowIndex -> rows[rowIndex].mapIndexed { columnIndex, cell -> cell.padEnd(columnWidths[columnIndex]) } } - .map { rowCells -> joinColumns(rowCells) } - .joinToString(separator = "\n") -} - -private fun printInColumns(groupedObjects: List>) = printInColumnsCustom(groupedObjects) { it.joinToString(separator = " | ", prefix = "| ", postfix = " |") } - -private class ActorWithResult(val actorRepresentation: String, val spacesAfterActor: Int, - val resultRepresentation: String, val spacesAfterResult: Int, - val clockRepresentation: String) { - override fun toString(): String = - actorRepresentation + ":" + " ".repeat(spacesAfterActor) + resultRepresentation + - " ".repeat(spacesAfterResult) + clockRepresentation -} - -private fun uniteActorsAndResultsLinear(actors: List, results: List): List { - require(actors.size == results.size) { - "Different numbers of actors and matching results found (${actors.size} != ${results.size})" - } - return actors.indices.map { - ActorWithResult("${actors[it]}", 1, "${results[it]}", 0, "") - } -} - -private fun uniteParallelActorsAndResults(actors: List>, results: List>): List> { - require(actors.size == results.size) { - "Different numbers of threads and matching results found (${actors.size} != ${results.size})" - } - return actors.mapIndexed { id, threadActors -> uniteActorsAndResultsAligned(threadActors, results[id]) } -} - -private fun uniteActorsAndResultsAligned(actors: List, results: List): List { - require(actors.size == results.size) { - "Different numbers of actors and matching results found (${actors.size} != ${results.size})" - } - val actorRepresentations = actors.map { it.toString() } - val resultRepresentations = results.map { it.result.toString() } - val maxActorLength = actorRepresentations.map { it.length }.max()!! - val maxResultLength = resultRepresentations.map { it.length }.max()!! - return actors.indices.map { i -> - val actorRepr = actorRepresentations[i] - val resultRepr = resultRepresentations[i] - val clock = results[i].clockOnStart - val spacesAfterActor = maxActorLength - actorRepr.length + 1 - val spacesAfterResultToAlign = maxResultLength - resultRepr.length - if (clock.empty) { - ActorWithResult(actorRepr, spacesAfterActor, resultRepr, spacesAfterResultToAlign, "") - } else { - ActorWithResult(actorRepr, spacesAfterActor, resultRepr, spacesAfterResultToAlign + 1, clock.toString()) - } - } -} - -internal fun StringBuilder.appendExecutionScenario(scenario: ExecutionScenario): StringBuilder { - if (scenario.initExecution.isNotEmpty()) { - appendln("Execution scenario (init part):") - appendln(scenario.initExecution) - } - if (scenario.parallelExecution.isNotEmpty()) { - appendln("Execution scenario (parallel part):") - append(printInColumns(scenario.parallelExecution)) - appendln() - } - if (scenario.postExecution.isNotEmpty()) { - appendln("Execution scenario (post part):") - append(scenario.postExecution) - } - return this +actual fun printErr(message: String) { + System.err.println(message) } -internal fun StringBuilder.appendFailure(failure: LincheckFailure): StringBuilder { +internal actual fun StringBuilder.appendFailure(failure: LincheckFailure): StringBuilder { when (failure) { is IncorrectResultsFailure -> appendIncorrectResultsFailure(failure) is DeadlockWithDumpFailure -> appendDeadlockWithDumpFailure(failure) @@ -160,30 +39,22 @@ internal fun StringBuilder.appendFailure(failure: LincheckFailure): StringBuilde } val results = if (failure is IncorrectResultsFailure) failure.results else null if (failure.trace != null) { - appendln() - appendln("= The following interleaving leads to the error =") + appendLine() + appendLine("= The following interleaving leads to the error =") appendTrace(failure.scenario, results, failure.trace) if (failure is DeadlockWithDumpFailure) { - appendln() + appendLine() append("All threads are in deadlock") } } return this } -private fun StringBuilder.appendUnexpectedExceptionFailure(failure: UnexpectedExceptionFailure): StringBuilder { - appendln("= The execution failed with an unexpected exception =") - appendExecutionScenario(failure.scenario) - appendln() - appendException(failure.exception) - return this -} - -private fun StringBuilder.appendDeadlockWithDumpFailure(failure: DeadlockWithDumpFailure): StringBuilder { +internal actual fun StringBuilder.appendDeadlockWithDumpFailure(failure: DeadlockWithDumpFailure): StringBuilder { appendLine("= The execution has hung, see the thread dump =") appendExecutionScenario(failure.scenario) appendLine() - for ((t, stackTrace) in failure.threadDump) { + for ((t, stackTrace) in failure.threadDump.dump) { val threadNumber = if (t is FixedActiveThreadsExecutor.TestThread) t.iThread.toString() else "?" appendLine("Thread-$threadNumber:") stackTrace.map { @@ -197,58 +68,9 @@ private fun StringBuilder.appendDeadlockWithDumpFailure(failure: DeadlockWithDum return this } -private fun StringBuilder.appendIncorrectResultsFailure(failure: IncorrectResultsFailure): StringBuilder { - appendln("= Invalid execution results =") - if (failure.scenario.initExecution.isNotEmpty()) { - appendln("Init part:") - appendln(uniteActorsAndResultsLinear(failure.scenario.initExecution, failure.results.initResults)) - } - if (failure.results.afterInitStateRepresentation != null) - appendln("STATE: ${failure.results.afterInitStateRepresentation}") - appendln("Parallel part:") - val parallelExecutionData = uniteParallelActorsAndResults(failure.scenario.parallelExecution, failure.results.parallelResultsWithClock) - append(printInColumns(parallelExecutionData)) - if (failure.results.afterParallelStateRepresentation != null) { - appendln() - append("STATE: ${failure.results.afterParallelStateRepresentation}") - } - if (failure.scenario.postExecution.isNotEmpty()) { - appendln() - appendln("Post part:") - append(uniteActorsAndResultsLinear(failure.scenario.postExecution, failure.results.postResults)) - } - if (failure.results.afterPostStateRepresentation != null && failure.scenario.postExecution.isNotEmpty()) { - appendln() - append("STATE: ${failure.results.afterPostStateRepresentation}") - } - if (failure.results.parallelResultsWithClock.flatten().any { !it.clockOnStart.empty }) - appendln("\n---\nvalues in \"[..]\" brackets indicate the number of completed operations \n" + - "in each of the parallel threads seen at the beginning of the current operation\n---") - return this -} - -private fun StringBuilder.appendValidationFailure(failure: ValidationFailure): StringBuilder { - appendln("= Validation function ${failure.functionName} has failed =") - appendExecutionScenario(failure.scenario) - appendException(failure.exception) - return this -} - -private fun StringBuilder.appendObstructionFreedomViolationFailure(failure: ObstructionFreedomViolationFailure): StringBuilder { - appendln("= ${failure.reason} =") - appendExecutionScenario(failure.scenario) - return this -} - -private fun StringBuilder.appendException(t: Throwable) { - val sw = StringWriter() - t.printStackTrace(PrintWriter(sw)) - appendln(sw.toString()) -} - -internal fun StringBuilder.appendStateEquivalenceViolationMessage(sequentialSpecification: Class<*>) { +internal actual fun StringBuilder.appendStateEquivalenceViolationMessage(sequentialSpecification: SequentialSpecification<*>) { append("To make verification faster, you can specify the state equivalence relation on your sequential specification.\n" + - "At the current moment, `${sequentialSpecification.simpleName}` does not specify it, or the equivalence relation implementation is incorrect.\n" + - "To fix this, please implement `equals()` and `hashCode()` functions on `${sequentialSpecification.simpleName}`; the simplest way is to extend `VerifierState`\n" + - "and override the `extractState()` function, which is called at once and the result of which is used for further `equals()` and `hashCode()` invocations.") + "At the current moment, `${sequentialSpecification.simpleName}` does not specify it, or the equivalence relation implementation is incorrect.\n" + + "To fix this, please implement `equals()` and `hashCode()` functions on `${sequentialSpecification.simpleName}`; the simplest way is to extend `VerifierState`\n" + + "and override the `extractState()` function, which is called at once and the result of which is used for further `equals()` and `hashCode()` invocations.") } \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt index cba7dd6d7..a410de5ee 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Result.kt @@ -1,14 +1,8 @@ -package org.jetbrains.kotlinx.lincheck - -import java.io.Serializable -import kotlin.coroutines.* - /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * Copyright (C) 2019 - 2021 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 @@ -21,30 +15,18 @@ import kotlin.coroutines.* * * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see - * . - * #L% + * */ -/** - * 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. - * - * [wasSuspended] is true if before getting this result the actor invocation suspended the thread. - * If result is [NoResult] and [wasSuspended] is true it means that - * the execution thread was suspended without any chance to be resumed, - * meaning that all other execution threads completed their execution or were suspended too. - */ -sealed class Result { - abstract val wasSuspended: Boolean - protected val wasSuspendedPrefix: String get() = (if (wasSuspended) "SUSPENDED + " else "") -} +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.* +import java.io.Serializable /** * Type of result used if the actor invocation returns any value. */ -class ValueResult @JvmOverloads constructor(val value: Any?, override val wasSuspended: Boolean = false) : Result() { +actual class ValueResult @JvmOverloads constructor(actual 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(value is Serializable?) { @@ -63,7 +45,7 @@ class ValueResult @JvmOverloads constructor(val value: Any?, override val wasSus override fun toString() = wasSuspendedPrefix + "$value" - override fun equals(other: Any?): Boolean { + actual 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 @@ -75,67 +57,9 @@ class ValueResult @JvmOverloads constructor(val value: Any?, override val wasSus else serializedObject.contentEquals(other.serializedObject) } - override fun hashCode(): Int = if (wasSuspended) 0 else 1 // we cannot use the value here -} - -/** - * Type of result used if the actor invocation does not return value. - */ -object VoidResult : Result() { - override val wasSuspended get() = false - override fun toString() = wasSuspendedPrefix + VOID -} - -object SuspendedVoidResult : Result() { - override val wasSuspended get() = true - override fun toString() = wasSuspendedPrefix + VOID + actual override fun hashCode(): Int = if (wasSuspended) 0 else 1 // we cannot use the value here } -private const val VOID = "void" - -object Cancelled : Result() { - override val wasSuspended get() = true - override fun toString() = wasSuspendedPrefix + "CANCELLED" -} - -/** - * Type of result used if the actor invocation fails with the specified in {@link Operation#handleExceptionsAsResult()} exception [tClazz]. - */ -@Suppress("DataClassPrivateConstructor") -data class ExceptionResult private constructor(val tClazz: Class, override val wasSuspended: Boolean) : Result() { - override fun toString() = wasSuspendedPrefix + tClazz.simpleName - - companion object { - @Suppress("UNCHECKED_CAST") - @JvmOverloads - fun create(tClazz: Class, wasSuspended: Boolean = false) = ExceptionResult(tClazz.normalize(), wasSuspended) - } -} // for byte-code generation -@JvmSynthetic -fun createExceptionResult(tClazz: Class) = ExceptionResult.create(tClazz, false) - -/** - * Type of result used if the actor invocation suspended the thread and did not get the final result yet - * though it can be resumed later - */ -object NoResult : Result() { - override val wasSuspended get() = false - override fun toString() = "-" -} - -object Suspended : Result() { - override val wasSuspended get() = true - override fun toString() = "S" -} - -/** - * Type of result used for verification. - * Resuming thread writes result of the suspension point and continuation to be executed in the resumed thread into [contWithSuspensionPointRes]. - */ -internal data class ResumedResult(val contWithSuspensionPointRes: Pair?, kotlin.Result>) : Result() { - override val wasSuspended: Boolean get() = true - - lateinit var resumedActor: Actor - lateinit var by: Actor -} \ No newline at end of file +@JvmOverloads +fun createExceptionResult(tClazz: Class, wasSuspended: Boolean = false) = ExceptionResult(tClazz.normalize().kotlin, false) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java index 0f7b9b4b0..abd557230 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/TransformationClassLoader.java @@ -37,7 +37,7 @@ import java.util.stream.Collectors; import static org.jetbrains.kotlinx.lincheck.TransformationClassLoader.*; -import static org.jetbrains.kotlinx.lincheck.UtilsKt.getCanonicalClassName; +import static org.jetbrains.kotlinx.lincheck.CommonUtilsKt.getCanonicalClassName; import static org.objectweb.asm.Opcodes.*; /** @@ -58,7 +58,7 @@ public TransformationClassLoader(Strategy strategy, Runner runner) { classTransformers = new ArrayList<>(); // Apply the strategy's transformer at first, then the runner's one. if (strategy.needsTransformation()) classTransformers.add(strategy::createTransformer); - if (runner.needsTransformation()) classTransformers.add(runner::createTransformer); + if (runner.needsTransformation()) classTransformers.add(cv -> (ClassVisitor)runner.createTransformer(cv)); remapper = UtilsKt.getRemapperByTransformers( // create transformers just for their class information classTransformers.stream() diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt index 20ea3653d..fbae80e18 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt @@ -22,7 +22,6 @@ package org.jetbrains.kotlinx.lincheck import kotlinx.coroutines.* -import org.jetbrains.kotlinx.lincheck.CancellableContinuationHolder.storedLastCancellableCont import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* @@ -36,20 +35,27 @@ import java.lang.reflect.Method import java.util.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* +import kotlin.reflect.KClass import kotlin.reflect.full.* import kotlin.reflect.jvm.* +actual class TestClass(val clazz: Class<*>) { + val name = clazz.name -fun chooseSequentialSpecification(sequentialSpecificationByUser: Class<*>?, testClass: Class<*>): Class<*> = - if (sequentialSpecificationByUser === DummySequentialSpecification::class.java || sequentialSpecificationByUser == null) testClass - else sequentialSpecificationByUser + actual fun createInstance(): Any = clazz.getDeclaredConstructor().newInstance() +} -internal fun executeActor(testInstance: Any, actor: Actor) = executeActor(testInstance, actor, null) +actual fun loadSequentialSpecification(sequentialSpecification: SequentialSpecification<*>): SequentialSpecification = + TransformationClassLoader { cv -> CancellabilitySupportClassTransformer(cv) }.loadClass(sequentialSpecification.name)!! + +actual fun chooseSequentialSpecification(sequentialSpecificationByUser: SequentialSpecification<*>?, testClass: TestClass): SequentialSpecification<*> = + if (sequentialSpecificationByUser === DummySequentialSpecification::class.java || sequentialSpecificationByUser == null) testClass.clazz + else sequentialSpecificationByUser /** * Executes the specified actor on the sequential specification instance and returns its result. */ -internal fun executeActor( +internal actual fun executeActor( instance: Any, actor: Actor, completion: Continuation? @@ -63,8 +69,8 @@ internal fun executeActor( } catch (invE: Throwable) { val eClass = (invE.cause ?: invE).javaClass.normalize() for (ec in actor.handledExceptions) { - if (ec.isAssignableFrom(eClass)) - return ExceptionResult.create(eClass) + if (ec.java.isAssignableFrom(eClass)) + return createExceptionResult(eClass) } throw IllegalStateException("Invalid exception as a result of $actor", invE) } catch (e: Exception) { @@ -77,18 +83,7 @@ internal fun executeActor( } } -internal inline fun executeValidationFunctions(instance: Any, validationFunctions: List, - onError: (functionName: String, exception: Throwable) -> Unit) { - for (f in validationFunctions) { - val validationException = executeValidationFunction(instance, f) - if (validationException != null) { - onError(f.name, validationException) - return - } - } -} - -private fun executeValidationFunction(instance: Any, validationFunction: Method): Throwable? { +internal actual fun executeValidationFunction(instance: Any, validationFunction: ValidationFunction): Throwable? { val m = getMethod(instance, validationFunction) try { m.invoke(instance) @@ -137,9 +132,9 @@ private fun Class.getMethod(name: String, parameterTypes: Array if (wasSuspended) SuspendedVoidResult else VoidResult - res != null && res is Throwable -> ExceptionResult.create(res.javaClass, wasSuspended) + res != null && res is Throwable -> createExceptionResult(res.javaClass, wasSuspended) res === COROUTINE_SUSPENDED -> Suspended res is kotlin.Result -> res.toLinCheckResult(wasSuspended) else -> ValueResult(res, wasSuspended) @@ -153,7 +148,7 @@ private fun kotlin.Result.toLinCheckResult(wasSuspended: Boolean) = is Throwable -> ValueResult(value::class.java, wasSuspended) else -> ValueResult(value, wasSuspended) } - } else ExceptionResult.create(exceptionOrNull()!!.let { it::class.java }, wasSuspended) + } else createExceptionResult(exceptionOrNull()!!.let { it::class.java }, wasSuspended) inline fun Throwable.catch(vararg exceptions: Class<*>, block: () -> R): R { if (exceptions.any { this::class.java.isAssignableFrom(it) }) { @@ -161,69 +156,10 @@ inline fun Throwable.catch(vararg exceptions: Class<*>, block: () -> R): R { } else throw this } -/** - * Returns scenario for the specified thread. Note that initial and post parts - * are represented as threads with ids `0` and `threads + 1` respectively. - */ -internal operator fun ExecutionScenario.get(threadId: Int): List = when (threadId) { - 0 -> initExecution - threads + 1 -> postExecution - else -> parallelExecution[threadId - 1] -} - -/** - * Returns results for the specified thread. Note that initial and post parts - * are represented as threads with ids `0` and `threads + 1` respectively. - */ -internal operator fun ExecutionResult.get(threadId: Int): List = when (threadId) { - 0 -> initResults - parallelResultsWithClock.size + 1 -> postResults - else -> parallelResultsWithClock[threadId - 1].map { it.result } -} - -internal class StoreExceptionHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { - var exception: Throwable? = null - - override fun handleException(context: CoroutineContext, exception: Throwable) { - this.exception = exception - } -} - -@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") -internal fun CancellableContinuation.cancelByLincheck(promptCancellation: Boolean): CancellationResult { - val exceptionHandler = context[CoroutineExceptionHandler] as StoreExceptionHandler - exceptionHandler.exception = null - val cancelled = cancel(cancellationByLincheckException) - exceptionHandler.exception?.let { - throw it.cause!! // let's throw the original exception, ignoring the internal coroutines details - } - return when { - cancelled -> CancellationResult.CANCELLED_BEFORE_RESUMPTION - promptCancellation -> { - context[Job]!!.cancel() // we should always put a job into the context for prompt cancellation - CancellationResult.CANCELLED_AFTER_RESUMPTION - } - else -> CancellationResult.CANCELLATION_FAILED - } -} - -internal enum class CancellationResult { CANCELLED_BEFORE_RESUMPTION, CANCELLED_AFTER_RESUMPTION, CANCELLATION_FAILED } - @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private val cancelCompletedResultMethod = DispatchedTask::class.declaredFunctions.find { it.name == "cancelCompletedResult" }!!.javaMethod!! -/** - * Returns `true` if the continuation was cancelled by [CancellableContinuation.cancel]. - */ -fun kotlin.Result.cancelledByLincheck() = exceptionOrNull() === cancellationByLincheckException - -private val cancellationByLincheckException = Exception("Cancelled by lincheck") - -object CancellableContinuationHolder { - var storedLastCancellableCont: CancellableContinuation<*>? = null -} - -fun storeCancellableContinuation(cont: CancellableContinuation<*>) { +actual fun storeCancellableContinuation(cont: CancellableContinuation<*>) { val t = Thread.currentThread() if (t is FixedActiveThreadsExecutor.TestThread) { t.cont = cont @@ -232,14 +168,14 @@ fun storeCancellableContinuation(cont: CancellableContinuation<*>) { } } -internal fun ExecutionScenario.convertForLoader(loader: ClassLoader) = ExecutionScenario( +internal actual fun ExecutionScenario.convertForLoader(loader: Any) = ExecutionScenario( initExecution, parallelExecution.map { actors -> actors.map { a -> - val args = a.arguments.map { it.convertForLoader(loader) } + val args = a.arguments.map { it.convertForLoader(loader as ClassLoader) } // the original `isSuspendable` is used here since `KFunction.isSuspend` fails on transformed classes Actor( - method = a.method.convertForLoader(loader), + method = a.method.convertForLoader(loader as ClassLoader), arguments = args, handledExceptions = a.handledExceptions, cancelOnSuspension = a.cancelOnSuspension, @@ -285,6 +221,9 @@ internal fun ByteArray.deserialize(loader: ClassLoader) = ByteArrayInputStream(t CustomObjectInputStream(loader, it).run { readObject() } } +internal fun getClassFromKClass(clazz: KClass) = clazz.java +internal fun getKClassFromClass(clazz: Class) = clazz.kotlin + /** * ObjectInputStream that uses custom class loader. */ @@ -301,9 +240,9 @@ private class CustomObjectInputStream(val loader: ClassLoader, inputStream: Inpu * Collects the current thread dump and keeps only those * threads that are related to the specified [runner]. */ -internal fun collectThreadDump(runner: Runner) = Thread.getAllStackTraces().filter { (t, _) -> +internal actual fun collectThreadDump(runner: Runner) = ThreadDump(Thread.getAllStackTraces().filter { (t, _) -> t is FixedActiveThreadsExecutor.TestThread && t.runnerHash == runner.hashCode() -} +}) /** * This method helps to encapsulate remapper logic from strategy interface. @@ -315,17 +254,4 @@ internal fun getRemapperByTransformers(classTransformers: List): R else -> null } -internal val String.canonicalClassName get() = this.replace('/', '.') -internal val String.internalClassName get() = this.replace('.', '/') - -fun wrapInvalidAccessFromUnnamedModuleExceptionWithDescription(e: Throwable): Throwable { - if (e.message?.contains("to unnamed module") ?: false) { - return RuntimeException(ADD_OPENS_MESSAGE, e) - } - return e -} - -private val ADD_OPENS_MESSAGE = "It seems that you use Java 9+ and the code uses Unsafe or similar constructions that are not accessible from unnamed modules.\n" + - "Please add the following lines to your test running configuration:\n" + - "--add-opens java.base/jdk.internal.misc=ALL-UNNAMED\n" + - "--add-exports java.base/jdk.internal.util=ALL-UNNAMED" \ No newline at end of file +internal actual fun nativeFreeze(any: Any) {} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/LogLevel.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/LogLevel.kt new file mode 100644 index 000000000..0d156a3e7 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/LogLevel.kt @@ -0,0 +1,34 @@ +/* +* #%L +* Lincheck +* %% +* Copyright (C) 2015 - 2018 Devexperts, LLC +* %% +* 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.annotations + +import org.jetbrains.kotlinx.lincheck.LoggingLevel +import java.lang.annotation.Inherited + +/** + * This annotation should be added to a test class to specify the logging level. + * By default, [LoggingLevel.ERROR] is used. + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +@Inherited +annotation class LogLevel(val value: LoggingLevel) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/OpGroupConfig.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/OpGroupConfig.java deleted file mode 100644 index d448a76e7..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/OpGroupConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.annotations; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Set some restrictions to the group with the specified name, - * used during the scenario generation phase. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Repeatable(OpGroupConfig.OpGroupConfigs.class) -@Inherited -public @interface OpGroupConfig { - /** - * Name of this group used by {@link Operation#group()}. - */ - String name() default ""; - - /** - * Set it to {@code true} for executing all actors in this group - * from one thread. This restriction allows to test single-reader - * and/or single-writer data structures and similar solutions. - */ - boolean nonParallel() default false; - - /** - * Holder annotation for {@link OpGroupConfig}. - * Not a public API. - */ - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - @Inherited - @interface OpGroupConfigs { - OpGroupConfig[] value(); - } -} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/OpGroupConfig.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/OpGroupConfig.kt new file mode 100644 index 000000000..42555e97a --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/OpGroupConfig.kt @@ -0,0 +1,54 @@ +/* +* #%L +* Lincheck +* %% +* Copyright (C) 2015 - 2018 Devexperts, LLC +* %% +* 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.annotations + +import java.lang.annotation.Inherited +import java.lang.annotation.Repeatable + +/** + * Set some restrictions to the group with the specified name, + * used during the scenario generation phase. + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +@Repeatable(OpGroupConfig.OpGroupConfigs::class) +@Inherited +annotation class OpGroupConfig constructor( + /** + * Name of this group used by [Operation.group]. + */ + val name: String = "", + /** + * Set it to `true` for executing all actors in this group + * from one thread. This restriction allows to test single-reader + * and/or single-writer data structures and similar solutions. + */ + val nonParallel: Boolean = false) { + /** + * Holder annotation for [OpGroupConfig]. + * Not a public API. + */ + @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) + @Inherited + annotation class OpGroupConfigs constructor(vararg val value: OpGroupConfig) +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Operation.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Operation.java deleted file mode 100644 index d2bbec600..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Operation.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.annotations; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import kotlinx.coroutines.*; -import java.lang.annotation.*; - -/** - * Mark your method with this annotation in order - * to use it in concurrent testing as an operation. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface Operation { - /** - * Binds the arguments of this operation with the specified {@link Param parameter configurations} - * by their {@link Param#name()} names. - */ - String[] params() default {}; - - /** - * Set it to {@code true} if you this operation should be called - * at most once during the test invocation; {@code false} by default. - */ - boolean runOnce() default false; - - /** - * Specifies the operation group which can add some execution restriction. - * @see OpGroupConfig#name() - */ - String group() default ""; - - /** - * Handle the specified exceptions as a result of this operation invocation. - */ - Class[] handleExceptionsAsResult() default {}; - - /** - * Specifies whether the operation can be cancelled if it suspends, - * see {@link CancellableContinuation#cancel}; {@code true} by default. - */ - boolean cancellableOnSuspension() default true; - - /** - * The operation marked with [allowExtraSuspension] is allowed to - * suspend (and, therefore, be cancelled if [cancellableOnSuspension] - * is set to `true`) even if it should not according to the sequential - * specification. The one may consider this as a relaxation of the - * dual data structures formalism. - */ - boolean allowExtraSuspension() default false; - - /** - * Specifies whether this operation is blocking. - * This way, if the test checks for a non-blocking progress guarantee, - * lincheck will not fail the test if a hang is detected on - * a running operations with this {@code blocking} marker. - */ - boolean blocking() default false; - - /** - * Specifies whether this operation invocation can lead - * to a blocking behavior of another concurrent operation. - * This way, if the test checks for a non-blocking progress guarantee, - * lincheck will not fail the test if a hang is detected - * while one of the operations marked with {@link #causesBlocking} - * is running concurrently. Note, that this operation is not - * considered as blocking until it is marked as {@link #blocking}. - */ - boolean causesBlocking() default false; - - /** - * Specifies whether this cancellable operation supports - * prompt cancellation, {@code false} by default. This parameter - * is ignored if {@link #cancellableOnSuspension} is {@code false}. - */ - boolean promptCancellation() default false; -} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Operation.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Operation.kt new file mode 100644 index 000000000..1a0f3fe81 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Operation.kt @@ -0,0 +1,89 @@ +/* +* #%L +* Lincheck +* %% +* Copyright (C) 2015 - 2018 Devexperts, LLC +* %% +* 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.annotations + +import kotlinx.coroutines.CancellableContinuation +import kotlin.reflect.KClass + +/** + * Mark your method with this annotation in order + * to use it in concurrent testing as an operation. + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +annotation class Operation( + /** + * Binds the arguments of this operation with the specified [parameter configurations][Param] + * by their [Param.name] names. + */ + val params: Array = [], + /** + * Set it to `true` if you this operation should be called + * at most once during the test invocation; `false` by default. + */ + val runOnce: Boolean = false, + /** + * Specifies the operation group which can add some execution restriction. + * @see OpGroupConfig.name + */ + val group: String = "", + /** + * Handle the specified exceptions as a result of this operation invocation. + */ + val handleExceptionsAsResult: Array> = [], + /** + * Specifies whether the operation can be cancelled if it suspends, + * see [CancellableContinuation.cancel]; `true` by default. + */ + val cancellableOnSuspension: Boolean = true, + /** + * The operation marked with [allowExtraSuspension] is allowed to + * suspend (and, therefore, be cancelled if [cancellableOnSuspension] + * is set to `true`) even if it should not according to the sequential + * specification. The one may consider this as a relaxation of the + * dual data structures formalism. + */ + val allowExtraSuspension: Boolean = false, + /** + * Specifies whether this operation is blocking. + * This way, if the test checks for a non-blocking progress guarantee, + * **lincheck** will not fail the test if a hang is detected on + * a running operations with this `blocking` marker. + */ + val blocking: Boolean = false, + /** + * Specifies whether this operation invocation can lead + * to a blocking behavior of another concurrent operation. + * This way, if the test checks for a non-blocking progress guarantee, + * **lincheck** will not fail the test if a hang is detected + * while one of the operations marked with [causesBlocking] + * is running concurrently. Note, that this operation is not + * considered as blocking until it is marked as [blocking]. + */ + val causesBlocking: Boolean = false, + /** + * Specifies whether this cancellable operation supports + * prompt cancellation, `false` by default. This parameter + * is ignored if [cancellableOnSuspension] is `false`. + */ + val promptCancellation: Boolean = false +) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Param.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Param.java deleted file mode 100644 index c5c594267..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Param.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.annotations; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import org.jetbrains.kotlinx.lincheck.paramgen.ParameterGenerator; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Use this annotation to specify parameter generators. - * @see ParameterGenerator - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER, ElementType.TYPE}) -@Repeatable(Param.Params.class) -@Inherited -public @interface Param { - /** - * If the annotation is set on a class, creates a {@link ParameterGenerator parameter generator} - * which can be used in {@link Operation operations} by this name. If is set on an operation, - * uses the specified named parameter generator which is created as described before. - */ - String name() default ""; - - /** - * Specifies the {@link ParameterGenerator} class which should be used for this parameter. - */ - Class> gen() default ParameterGenerator.Dummy.class; - - /** - * Specifies the configuration for the {@link #gen() parameter generator}. - */ - String conf() default ""; - - /** - * Holder annotation for {@link Param}. - * Not a public API. - */ - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - @Inherited - @interface Params { - Param[] value(); - } -} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Param.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Param.kt new file mode 100644 index 000000000..521b8184d --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/Param.kt @@ -0,0 +1,61 @@ +/* + * 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.annotations + +import org.jetbrains.kotlinx.lincheck.paramgen.ParameterGenerator +import org.jetbrains.kotlinx.lincheck.paramgen.ParameterGenerator.Dummy +import java.lang.annotation.Inherited +import java.lang.annotation.Repeatable +import kotlin.reflect.KClass + +/** + * Use this annotation to specify parameter generators. + * @see ParameterGenerator + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +@Repeatable(Param.Params::class) +@Inherited +annotation class Param constructor( + /** + * If the annotation is set on a class, creates a [parameter generator][ParameterGenerator] + * which can be used in [operations][Operation] by this name. If is set on an operation, + * uses the specified named parameter generator which is created as described before. + */ + val name: String = "", + /** + * Specifies the [ParameterGenerator] class which should be used for this parameter. + */ + val gen: KClass> = Dummy::class, + /** + * Specifies the configuration for the [parameter generator][.gen]. + */ + val conf: String = "" +) { + /** + * Holder annotation for [Param]. + * Not a public API. + */ + @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) + @Inherited + annotation class Params constructor(vararg val value: Param) +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/StateRepresentation.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/StateRepresentation.kt index 821291645..6ec123b76 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/StateRepresentation.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/annotations/StateRepresentation.kt @@ -1,23 +1,21 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.annotations @@ -27,7 +25,7 @@ package org.jetbrains.kotlinx.lincheck.annotations * each meaningful event (e.g., write to atomic variable or function that potentially * changes the data structure call). In order to specify the way for representing * the data structure state, a public no-argument function that returns [String] - * should be marked with this annotation. + * should be marked with this annotation. */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt index 3d9dc090c..7c54703c3 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt @@ -1,40 +1,38 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.execution import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.paramgen.* import java.lang.reflect.* -import kotlin.random.* +import kotlin.reflect.KClass /** * Implementations of this class generate [actors][Actor] * using [parameter generators][ParameterGenerator]. */ -class ActorGenerator( +actual class ActorGenerator( private val method: Method, private val parameterGenerators: List>, - private val handledExceptions: List>, - val useOnce: Boolean, + actual val handledExceptions: List>, + actual val useOnce: Boolean, cancellableOnSuspension: Boolean, private val allowExtraSuspension: Boolean, private val blocking: Boolean, @@ -44,7 +42,7 @@ class ActorGenerator( private val cancellableOnSuspension = cancellableOnSuspension && isSuspendable private val promptCancellation = cancellableOnSuspension && promptCancellation - fun generate(threadId: Int): Actor { + actual fun generate(threadId: Int): Actor { val parameters = parameterGenerators .map { it.generate() } .map { if (it === THREAD_ID_TOKEN) threadId else it } @@ -62,8 +60,6 @@ class ActorGenerator( ) } - val isSuspendable: Boolean get() = method.isSuspendable() - override fun toString() = method.toString() + actual val isSuspendable: Boolean get() = method.isSuspendable() + actual override fun toString() = method.toString() } - -private val DETERMINISTIC_RANDOM = Random(42) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionScenario.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionScenario.java deleted file mode 100644 index 7b5e02c79..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/ExecutionScenario.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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.execution; - -import org.jetbrains.kotlinx.lincheck.Actor; -import org.jetbrains.kotlinx.lincheck.strategy.Strategy; - -import java.util.*; -import java.util.stream.*; - -import static org.jetbrains.kotlinx.lincheck.ReporterKt.appendExecutionScenario; - -/** - * This class represents an execution scenario, which - * is generated by an {@link ExecutionGenerator} and then \ - * used by a {@link Strategy} which produces an {@link ExecutionResult}. - */ -public class ExecutionScenario { - /** - * The initial sequential part of the execution. - * It helps to produce different initial states - * before the parallel part. - * - * The initial execution part should contain only non-suspendable actors; - * otherwise, the single initial execution thread will suspend with no chance to be resumed. - */ - public final List initExecution; - /** - * The parallel part of the execution, which is used - * to find an interleaving with incorrect behaviour. - */ - public final List> parallelExecution; - /** - * The last sequential part is used to test that - * the data structure is in some correct state. - * - * If this execution scenario contains suspendable actors, the post part should be empty; - * if not, an actor could resume a previously suspended one from the parallel execution part. - */ - public final List postExecution; - - public ExecutionScenario(List initExecution, List> parallelExecution, List postExecution) { - this.initExecution = initExecution; - this.parallelExecution = parallelExecution; - this.postExecution = postExecution; - } - - /** - * Returns the number of threads used in the parallel part of this execution. - */ - public int getThreads() { - return parallelExecution.size(); - } - - /** - * Returns `true` if there is at least one suspendable actor in the generated scenario - */ - public boolean hasSuspendableActors() { - return Stream.concat(parallelExecution.stream().flatMap(Collection::stream), postExecution.stream()).anyMatch(Actor::isSuspendable); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - appendExecutionScenario(sb, this); - return sb.toString(); - } -} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/RandomExecutionGenerator.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/RandomExecutionGenerator.java deleted file mode 100644 index 2cd8a460c..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/execution/RandomExecutionGenerator.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.execution; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import org.jetbrains.kotlinx.lincheck.Actor; -import org.jetbrains.kotlinx.lincheck.CTestConfiguration; -import org.jetbrains.kotlinx.lincheck.CTestStructure; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Random; -import java.util.stream.Collectors; - -public class RandomExecutionGenerator extends ExecutionGenerator { - private final Random random = new Random(0); - - public RandomExecutionGenerator(CTestConfiguration testConfiguration, CTestStructure testStructure) { - super(testConfiguration, testStructure); - } - - @Override - public ExecutionScenario nextExecution() { - // Create init execution part - List validActorGeneratorsForInit = testStructure.actorGenerators.stream() - .filter(ag -> !ag.getUseOnce() && !ag.isSuspendable()).collect(Collectors.toList()); - List initExecution = new ArrayList<>(); - for (int i = 0; i < testConfiguration.getActorsBefore() && !validActorGeneratorsForInit.isEmpty(); i++) { - ActorGenerator ag = validActorGeneratorsForInit.get(random.nextInt(validActorGeneratorsForInit.size())); - initExecution.add(ag.generate(0)); - } - // Create parallel execution part - // Construct non-parallel groups and parallel one - List nonParallelGroups = testStructure.operationGroups.stream() - .filter(g -> g.nonParallel) - .collect(Collectors.toList()); - Collections.shuffle(nonParallelGroups); - List parallelGroup = new ArrayList<>(testStructure.actorGenerators); - nonParallelGroups.forEach(g -> parallelGroup.removeAll(g.actors)); - - List> parallelExecution = new ArrayList<>(); - List threadGens = new ArrayList<>(); - for (int t = 0; t < testConfiguration.getThreads(); t++) { - parallelExecution.add(new ArrayList<>()); - threadGens.add(new ThreadGen(t, testConfiguration.getActorsPerThread())); - } - for (int i = 0; i < nonParallelGroups.size(); i++) { - threadGens.get(i % threadGens.size()).nonParallelActorGenerators - .addAll(nonParallelGroups.get(i).actors); - } - List tgs2 = new ArrayList<>(threadGens); - while (!threadGens.isEmpty()) { - for (Iterator it = threadGens.iterator(); it.hasNext(); ) { - ThreadGen threadGen = it.next(); - int aGenIndexBound = threadGen.nonParallelActorGenerators.size() + parallelGroup.size(); - if (aGenIndexBound == 0) { - it.remove(); - continue; - } - int aGenIndex = random.nextInt(aGenIndexBound); - ActorGenerator agen; - if (aGenIndex < threadGen.nonParallelActorGenerators.size()) { - agen = getActorGenFromGroup(threadGen.nonParallelActorGenerators, aGenIndex); - } else { - agen = getActorGenFromGroup(parallelGroup, - aGenIndex - threadGen.nonParallelActorGenerators.size()); - } - parallelExecution.get(threadGen.iThread).add(agen.generate(threadGen.iThread + 1)); - if (--threadGen.left == 0) - it.remove(); - } - } - parallelExecution = parallelExecution.stream().filter(actors -> !actors.isEmpty()).collect(Collectors.toList()); - // Create post execution part if the parallel part does not have suspendable actors - List postExecution; - if (parallelExecution.stream().noneMatch(actors -> actors.stream().anyMatch(Actor::isSuspendable))) { - postExecution = new ArrayList<>(); - List leftActorGenerators = new ArrayList<>(parallelGroup); - for (ThreadGen threadGen : tgs2) - leftActorGenerators.addAll(threadGen.nonParallelActorGenerators); - for (int i = 0; i < testConfiguration.getActorsAfter() && !leftActorGenerators.isEmpty(); i++) { - ActorGenerator agen = getActorGenFromGroup(leftActorGenerators, random.nextInt(leftActorGenerators.size())); - postExecution.add(agen.generate(testConfiguration.getThreads() + 1)); - } - } else { - postExecution = Collections.emptyList(); - } - return new ExecutionScenario(initExecution, parallelExecution, postExecution); - } - - private ActorGenerator getActorGenFromGroup(List aGens, int index) { - ActorGenerator aGen = aGens.get(index); - if (aGen.getUseOnce()) - aGens.remove(index); - return aGen; - } - - private static class ThreadGen { - final List nonParallelActorGenerators = new ArrayList<>(); - int iThread; - int left; - - ThreadGen(int iThread, int nActors) { - this.iThread = iThread; - this.left = nActors; - } - } -} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/DoubleGen.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/DoubleGen.java deleted file mode 100644 index 1eab85f4d..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/DoubleGen.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import java.util.Random; - -public class DoubleGen implements ParameterGenerator { - private static final float DEFAULT_BEGIN = -10; - private static final float DEFAULT_END = 10; - private static final float DEFAULT_STEP = 0.1f; - - private final Random random = new Random(0); - private final double begin; - private final double end; - private final double step; - - public DoubleGen(String configuration) { - if (configuration.isEmpty()) { // use default configuration - begin = DEFAULT_BEGIN; - end = DEFAULT_END; - step = DEFAULT_STEP; - return; - } - String[] args = configuration.replaceAll("\\s", "").split(":"); - switch (args.length) { - case 2: // begin:end - begin = Double.parseDouble(args[0]); - end = Double.parseDouble(args[1]); - step = DEFAULT_STEP; - break; - case 3: // begin:step:end - begin = Double.parseDouble(args[0]); - step = Double.parseDouble(args[1]); - end = Double.parseDouble(args[2]); - break; - default: - throw new IllegalArgumentException("Configuration should have two (begin and end) " + - "or three (begin, step and end) arguments separated by colon"); - } - if ((end - begin) / step >= Integer.MAX_VALUE) - throw new IllegalArgumentException("step is too small for specified range"); - } - - public Double generate() { - double delta = end - begin; - if (step == 0) // step is not defined - return begin + delta * random.nextDouble(); - int maxSteps = (int) (delta / step); - return begin + delta * random.nextInt(maxSteps + 1); - } -} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/IntGen.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/IntGen.java deleted file mode 100644 index 59a3c7f1a..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/IntGen.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import java.util.Random; - -public class IntGen implements ParameterGenerator { - private static final int DEFAULT_BEGIN = -10; - private static final int DEFAULT_END = 10; - - private final Random random = new Random(0); - private final int begin; - private final int end; - - public IntGen(String configuration) { - if (configuration.isEmpty()) { // use default configuration - begin = DEFAULT_BEGIN; - end = DEFAULT_END; - return; - } - String[] args = configuration.replaceAll("\\s", "").split(":"); - switch (args.length) { - case 2: // begin:end - begin = Integer.parseInt(args[0]); - end = Integer.parseInt(args[1]); - break; - default: - throw new IllegalArgumentException("Configuration should have " + - "two arguments (begin and end) separated by colon"); - } - } - - public Integer generate() { - return begin + random.nextInt(end - begin + 1); - } - - void checkRange(int min, int max, String type) { - if (this.begin < min || this.end - 1 > max) { - throw new IllegalArgumentException("Illegal range for " - + type + " type: [" + begin + "; " + end + ")"); - } - } -} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/StringGen.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/StringGen.java deleted file mode 100644 index 292b7ce26..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/StringGen.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import java.util.Random; - -public class StringGen implements ParameterGenerator { - private static final int DEFAULT_MAX_WORD_LENGTH = 15; - private static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ "; - - private final Random random = new Random(0); - private final int maxWordLength; - private final String alphabet; - - public StringGen(String configuration) { - if (configuration.isEmpty()) { // use default configuration - maxWordLength = DEFAULT_MAX_WORD_LENGTH; - alphabet = DEFAULT_ALPHABET; - return; - } - int firstCommaIndex = configuration.indexOf(':'); - if (firstCommaIndex < 0) { // maxWordLength only - maxWordLength = Integer.parseInt(configuration); - alphabet = DEFAULT_ALPHABET; - } else { // maxWordLength:alphabet - maxWordLength = Integer.parseInt(configuration.substring(0, firstCommaIndex)); - alphabet = configuration.substring(firstCommaIndex + 1); - } - } - - public String generate() { - char[] cs = new char[random.nextInt(maxWordLength)]; - for (int i = 0; i < cs.length; i++) - cs[i] = alphabet.charAt(random.nextInt(alphabet.length())); - return new String(cs); - } -} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt index 2a59c0ee8..0b6b73b15 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt @@ -8,12 +8,12 @@ * 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 * . @@ -88,8 +88,8 @@ internal class FixedActiveThreadsExecutor(private val nThreads: Int, runnerHash: * and waits until all of them are completed. * The number of tasks should be equal to [nThreads]. * - * @throws TimeoutException if more than [timeoutMs] is passed. - * @throws ExecutionException if an unexpected exception is thrown during the execution. + * @throws LincheckTimeoutException if more than [timeoutMs] is passed. + * @throws LincheckExecutionException if an unexpected exception is thrown during the execution. */ fun submitAndAwait(tasks: Array, timeoutMs: Long) { require(tasks.size == nThreads) @@ -140,7 +140,7 @@ internal class FixedActiveThreadsExecutor(private val nThreads: Int, runnerHash: private fun awaitTask(iThread: Int, deadline: Long) { val result = getResult(iThread, deadline) // Check whether there was an exception during the execution. - if (result != DONE) throw ExecutionException(result as Throwable) + if (result != DONE) throw LincheckExecutionException(result as Throwable) } private fun getResult(iThread: Int, deadline: Long): Any { @@ -155,7 +155,7 @@ internal class FixedActiveThreadsExecutor(private val nThreads: Int, runnerHash: val timeLeft = deadline - System.currentTimeMillis() if (timeLeft <= 0) { hangDetected = true - throw TimeoutException() + throw LincheckTimeoutException() } LockSupport.parkNanos(timeLeft * 1_000_000) } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/InvocationResult.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/InvocationResult.kt index b572f4e43..9564f1f6d 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/InvocationResult.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/InvocationResult.kt @@ -1,9 +1,8 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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 @@ -16,54 +15,9 @@ * * 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.runner - -import org.jetbrains.kotlinx.lincheck.execution.* - -/** - * Represents results for invocations, see [Runner.run]. - */ -sealed class InvocationResult - -/** - * The invocation completed successfully, the output [results] are provided. + * */ -class CompletedInvocationResult( - val results: ExecutionResult -) : InvocationResult() -/** - * Indicates that the invocation has run into deadlock or livelock. - */ -class DeadlockInvocationResult( - val threadDump: Map> -) : InvocationResult() - -/** - * The invocation has completed with an unexpected exception. - */ -class UnexpectedExceptionInvocationResult( - val exception: Throwable -) : InvocationResult() - -/** - * The invocation successfully completed, but the - * [validation function][org.jetbrains.kotlinx.lincheck.annotations.Validate] - * check failed. - */ -class ValidationFailureInvocationResult( - val scenario: ExecutionScenario, - val functionName: String, - val exception: Throwable -) : InvocationResult() +package org.jetbrains.kotlinx.lincheck.runner -/** - * Obstruction freedom check is requested, - * but an invocation that hangs has been found. - */ -class ObstructionFreedomViolationInvocationResult( - val reason: String -) : InvocationResult() \ No newline at end of file +actual class ThreadDump(val dump: Map>) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt index 34d9d9ed9..3b068eaf6 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt @@ -8,12 +8,12 @@ * 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 * . @@ -44,16 +44,18 @@ private typealias SuspensionPointResultWithContinuation = AtomicReference, - validationFunctions: List, - stateRepresentationFunction: Method?, + testClass: TestClass, + validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, private val timeoutMs: Long, // for deadlock or livelock detection - private val useClocks: UseClocks // specifies whether `HBClock`-s should always be used or with some probability + private val useClocks: UseClocks, // specifies whether `HBClock`-s should always be used or with some probability + initThreadFunction: (() -> Unit)?, + finishThreadFunction: (() -> Unit)? ) : Runner(strategy, testClass, validationFunctions, stateRepresentationFunction) { private val runnerHash = this.hashCode() // helps to distinguish this runner threads from others - private val executor = FixedActiveThreadsExecutor(scenario.threads, runnerHash) // shoukd be closed in `close()` + private val executor = FixedActiveThreadsExecutor(scenario.threads, runnerHash) // should be closed in `close()` private lateinit var testInstance: Any private lateinit var testThreadExecutions: Array @@ -143,14 +145,14 @@ internal open class ParallelThreadsRunner( } private fun reset() { - testInstance = testClass.newInstance() + testInstance = testClass.createInstance() testThreadExecutions.forEachIndexed { t, ex -> ex.testInstance = testInstance val threads = scenario.threads val actors = scenario.parallelExecution[t].size ex.useClocks = if (useClocks == ALWAYS) true else Random.nextBoolean() ex.curClock = 0 - ex.clocks = Array(actors) { emptyClockArray(threads) } + ex.clocks = Array(actors) { emptyClockArray(threads).toArray() } ex.results = arrayOfNulls(actors) } suspensionPointResults.forEach { it.fill(NoResult) } @@ -261,14 +263,14 @@ internal open class ParallelThreadsRunner( val afterInitStateRepresentation = constructStateRepresentation() try { executor.submitAndAwait(testThreadExecutions, timeoutMs) - } catch (e: TimeoutException) { + } catch (e: LincheckTimeoutException) { val threadDump = collectThreadDump(this) return DeadlockInvocationResult(threadDump) - } catch (e: ExecutionException) { + } catch (e: LincheckExecutionException) { return UnexpectedExceptionInvocationResult(e.cause!!) } val parallelResultsWithClock = testThreadExecutions.map { ex -> - ex.results.zip(ex.clocks).map { ResultWithClock(it.first, HBClock(it.second)) } + ex.results.zip(ex.clocks).map { ResultWithClock(it.first, HBClock(it.second.toLincheckAtomicIntArray())) } } executeValidationFunctions(testInstance, validationFunctions) { functionName, exception -> val s = ExecutionScenario( @@ -325,7 +327,7 @@ internal open class ParallelThreadsRunner( } override fun needsTransformation() = true - override fun createTransformer(cv: ClassVisitor) = CancellabilitySupportClassTransformer(cv) + override fun createTransformer(cv: Any) = CancellabilitySupportClassTransformer(cv as ClassVisitor) override fun constructStateRepresentation() = stateRepresentationFunction?.let{ getMethod(testInstance, it) }?.invoke(testInstance) as String? @@ -336,8 +338,4 @@ internal open class ParallelThreadsRunner( } } -internal enum class UseClocks { ALWAYS, RANDOM } - -internal enum class CompletionStatus { CANCELLED, RESUMED } - private const val MAX_SPINNING_TIME_BEFORE_YIELD = 2_000_000 \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt index ff5ffd6f6..167a25351 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/Runner.kt @@ -1,156 +1,38 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.runner import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.strategy.* -import org.objectweb.asm.* -import java.lang.reflect.* -import java.util.concurrent.atomic.* -import org.jetbrains.kotlinx.lincheck.annotations.StateRepresentation -import java.io.* - -/** - * Runner determines how to run your concurrent test. In order to support techniques - * like fibers, it may require code transformation, so that [createTransformer] should - * provide the corresponding transformer and [needsTransformation] should return `true`. - */ -abstract class Runner protected constructor( - protected val strategy: Strategy, - private val _testClass: Class<*>, // will be transformed later - protected val validationFunctions: List, - protected val stateRepresentationFunction: Method? -) : Closeable { - protected var scenario = strategy.scenario // `strategy.scenario` will be transformed in `initialize` - protected lateinit var testClass: Class<*> // not available before `initialize` call - @Suppress("LeakingThis") - val classLoader: ExecutionClassLoader = if (needsTransformation() || strategy.needsTransformation()) TransformationClassLoader(strategy, this) - else ExecutionClassLoader() - protected val completedOrSuspendedThreads = AtomicInteger(0) - - /** - * This method is a part of `Runner` initialization and should be invoked after this runner - * creation. It is separated from the constructor to perform the strategy initialization at first. - */ - open fun initialize() { - scenario = strategy.scenario.convertForLoader(classLoader) - testClass = loadClass(_testClass.typeName) - } - - /** - * Returns the current state representation of the test instance constructed via - * the function marked with [StateRepresentation] annotation, or `null` - * if no such function is provided. - * - * Please note, that it is unsafe to call this method concurrently with the running scenario. - * However, it is fine to call it if the execution is paused somewhere in the middle. - */ - open fun constructStateRepresentation(): String? = null - - /** - * Loads the specified class via this runner' class loader. - */ - private fun loadClass(className: String): Class<*> = classLoader.loadClass(className) - - /** - * Creates a transformer required for this runner. - * Throws [UnsupportedOperationException] by default. - * - * @return class visitor which transform the code due to support this runner. - */ - open fun createTransformer(cv: ClassVisitor): ClassVisitor? = null - /** - * This method should return `true` if code transformation - * is required for this runner; returns `false` by default. - */ - open fun needsTransformation(): Boolean = false +actual fun Runner.loadClassLoader(): Any = + if (needsTransformation() || strategy.needsTransformation()) TransformationClassLoader(strategy, this) else ExecutionClassLoader() - /** - * Runs the next invocation. - */ - abstract fun run(): InvocationResult +actual fun loadClass(classLoader: Any, testClass: TestClass): TestClass = TestClass((classLoader as ClassLoader).loadClass(testClass.clazz.typeName)) - /** - * This method is invoked by every test thread as the first operation. - * @param iThread number of invoking thread - */ - open fun onStart(iThread: Int) {} +actual typealias AtomicInteger = java.util.concurrent.atomic.AtomicInteger - /** - * This method is invoked by every test thread as the last operation - * if no exception has been thrown. - * @param iThread number of invoking thread - */ - open fun onFinish(iThread: Int) {} - - /** - * This method is invoked by the corresponding test thread - * when an unexpected exception is thrown. - */ - open fun onFailure(iThread: Int, e: Throwable) {} - - /** - * This method is invoked by the corresponding test thread - * when the current coroutine suspends. - * @param iThread number of invoking thread - */ - open fun afterCoroutineSuspended(iThread: Int): Unit = throw UnsupportedOperationException("Coroutines are not supported") - - /** - * This method is invoked by the corresponding test thread - * when the current coroutine is resumed. - */ - open fun afterCoroutineResumed(iThread: Int): Unit = throw UnsupportedOperationException("Coroutines are not supported") - - /** - * This method is invoked by the corresponding test thread - * when the current coroutine is cancelled. - */ - open fun afterCoroutineCancelled(iThread: Int): Unit = throw UnsupportedOperationException("Coroutines are not supported") - - /** - * Returns `true` if the coroutine corresponding to - * the actor `actorId` in the thread `iThread` is resumed. - */ - open fun isCoroutineResumed(iThread: Int, actorId: Int): Boolean = throw UnsupportedOperationException("Coroutines are not supported") - - /** - * Is invoked before each actor execution from the specified thread. - * The invocations are inserted into the generated code. - */ - fun onActorStart(iThread: Int) { - strategy.onActorStart(iThread) +fun Runner.use(block: (Runner) -> R): R { + try { + return block(this) + } finally { + this.close() } - - /** - * Closes the resources used in this runner. - */ - override fun close() {} - - /** - * @return whether all scenario threads are completed or suspended - * Used by generated code. - */ - val isParallelExecutionCompleted: Boolean - get() = completedOrSuspendedThreads.get() == scenario.threads -} +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.java index 0c72ab8dc..a1ebc21ba 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.java +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.java @@ -1,9 +1,8 @@ /* - * #%L * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% + * + * Copyright (C) 2019 - 2021 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 @@ -16,9 +15,9 @@ * * 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.runner; import org.jetbrains.kotlinx.lincheck.Result; diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecutionGenerator.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecutionGenerator.java index a68fd33b6..db1da0c0b 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecutionGenerator.java +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecutionGenerator.java @@ -23,6 +23,7 @@ */ import kotlin.coroutines.Continuation; +import kotlin.reflect.KClass; import org.jetbrains.kotlinx.lincheck.*; import org.jetbrains.kotlinx.lincheck.runner.ParallelThreadsRunner.*; import org.objectweb.asm.*; @@ -31,9 +32,11 @@ import org.objectweb.asm.commons.TryCatchBlockSorter; import org.objectweb.asm.util.CheckClassAdapter; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; +import static org.jetbrains.kotlinx.lincheck.UtilsKt.getClassFromKClass; import static org.objectweb.asm.Opcodes.*; import static org.objectweb.asm.Type.*; @@ -109,14 +112,14 @@ public static TestThreadExecution create(Runner runner, int iThread, List String className = TestThreadExecution.class.getCanonicalName() + generatedClassNumber++; String internalClassName = className.replace('.', '/'); List objArgs = new ArrayList<>(); - Class clz = runner.getClassLoader().defineClass(className, - generateClass(internalClassName, getType(runner.getTestClass()), iThread, actors, objArgs, completions, scenarioContainsSuspendableActors)); + Class clz = ((ExecutionClassLoader)runner.getClassLoader()).defineClass(className, + generateClass(internalClassName, getType(runner.getTestClass().getClazz()), iThread, actors, objArgs, completions, scenarioContainsSuspendableActors)); try { - TestThreadExecution execution = clz.newInstance(); + TestThreadExecution execution = clz.getDeclaredConstructor().newInstance(); execution.runner = runner; execution.objArgs = objArgs.toArray(); return execution; - } catch (InstantiationException | IllegalAccessException e) { + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { throw new IllegalStateException("Cannot initialize generated execution class", e); } } @@ -190,8 +193,8 @@ private static void generateRun(ClassVisitor cv, Type testType, int iThread, Lis Label actorCatchBlockEnd = mv.newLabel(); if (actor.getHandlesExceptions()) { handledExceptionHandler = mv.newLabel(); - for (Class ec : actor.getHandledExceptions()) - mv.visitTryCatchBlock(actorCatchBlockStart, actorCatchBlockEnd, handledExceptionHandler, getType(ec).getInternalName()); + for (KClass ec : actor.getHandledExceptions()) + mv.visitTryCatchBlock(actorCatchBlockStart, actorCatchBlockEnd, handledExceptionHandler, getType(getClassFromKClass(ec)).getInternalName()); } // Catch those exceptions that has not been caught yet Label unexpectedExceptionHandler = mv.newLabel(); diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/LincheckFailure.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/LincheckFailure.kt index 3248c2c09..8d4954630 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/LincheckFailure.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/LincheckFailure.kt @@ -1,73 +1,24 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.strategy -import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.execution.* -import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* -sealed class LincheckFailure( - val scenario: ExecutionScenario, - val trace: Trace? -) { - override fun toString() = StringBuilder().appendFailure(this).toString() -} - -internal class IncorrectResultsFailure( - scenario: ExecutionScenario, - val results: ExecutionResult, - trace: Trace? = null -) : LincheckFailure(scenario, trace) - -internal class DeadlockWithDumpFailure( - scenario: ExecutionScenario, - val threadDump: Map>, - trace: Trace? = null -) : LincheckFailure(scenario, trace) - -internal class UnexpectedExceptionFailure( - scenario: ExecutionScenario, - val exception: Throwable, - trace: Trace? = null -) : LincheckFailure(scenario, trace) - -internal class ValidationFailure( - scenario: ExecutionScenario, - val functionName: String, - val exception: Throwable, - trace: Trace? = null -) : LincheckFailure(scenario, trace) - -internal class ObstructionFreedomViolationFailure( - scenario: ExecutionScenario, - val reason: String, - trace: Trace? = null -) : LincheckFailure(scenario, trace) - -internal fun InvocationResult.toLincheckFailure(scenario: ExecutionScenario, trace: Trace? = null) = when (this) { - is DeadlockInvocationResult -> DeadlockWithDumpFailure(scenario, threadDump, trace) - is UnexpectedExceptionInvocationResult -> UnexpectedExceptionFailure(scenario, exception, trace) - is ValidationFailureInvocationResult -> ValidationFailure(scenario, functionName, exception, trace) - is ObstructionFreedomViolationInvocationResult -> ObstructionFreedomViolationFailure(scenario, reason, trace) - else -> error("Unexpected invocation result type: ${this.javaClass.simpleName}") -} \ No newline at end of file +actual data class Trace(val trace: List, val verboseTrace: Boolean) \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt index 384836aa8..681b339f1 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt @@ -23,7 +23,6 @@ package org.jetbrains.kotlinx.lincheck.strategy import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario import org.objectweb.asm.ClassVisitor -import org.objectweb.asm.commons.Remapper /** * Implementation of this class describes how to run the generated execution. @@ -32,18 +31,18 @@ import org.objectweb.asm.commons.Remapper * [.createStrategy] method is used. It is impossible to add a new strategy * without any code change. */ -abstract class Strategy protected constructor( - val scenario: ExecutionScenario +actual abstract class Strategy protected actual constructor( + actual val scenario: ExecutionScenario ) { open fun needsTransformation() = false open fun createTransformer(cv: ClassVisitor): ClassVisitor { throw UnsupportedOperationException("$javaClass strategy does not transform classes") } - abstract fun run(): LincheckFailure? + actual abstract fun run(): LincheckFailure? /** * Is invoked before each actor execution. */ - open fun onActorStart(iThread: Int) {} + actual open fun onActorStart(iThread: Int) {} } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedCTestConfiguration.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedCTestConfiguration.kt index e30f38ad3..840fb309e 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedCTestConfiguration.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedCTestConfiguration.kt @@ -21,7 +21,6 @@ */ package org.jetbrains.kotlinx.lincheck.strategy.managed -import kotlinx.coroutines.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.verifier.* @@ -30,15 +29,15 @@ import org.jetbrains.kotlinx.lincheck.verifier.* * A common configuration for managed strategies. */ abstract class ManagedCTestConfiguration( - testClass: Class<*>, iterations: Int, + testClass: TestClass, iterations: Int, threads: Int, actorsPerThread: Int, actorsBefore: Int, actorsAfter: Int, - generatorClass: Class, verifierClass: Class, + executionGenerator: (testConfiguration: CTestConfiguration, testStructure: CTestStructure) -> ExecutionGenerator, verifierClass: (sequentialSpecification: SequentialSpecification<*>) -> Verifier, val checkObstructionFreedom: Boolean, val hangingDetectionThreshold: Int, val invocationsPerIteration: Int, val guarantees: List, requireStateEquivalenceCheck: Boolean, minimizeFailedScenario: Boolean, - sequentialSpecification: Class<*>, timeoutMs: Long, val eliminateLocalObjects: Boolean, val verboseTrace: Boolean, + sequentialSpecification: SequentialSpecification<*>, timeoutMs: Long, val eliminateLocalObjects: Boolean, val verboseTrace: Boolean, customScenarios: List ) : CTestConfiguration( - testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, generatorClass, verifierClass, + testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, executionGenerator, verifierClass, requireStateEquivalenceCheck, minimizeFailedScenario, sequentialSpecification, timeoutMs, customScenarios ) { companion object { diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt index f6f53234e..eac819a92 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt @@ -30,7 +30,6 @@ import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.objectweb.asm.* import java.io.* -import java.lang.reflect.* import java.util.* import kotlin.collections.set @@ -43,11 +42,11 @@ import kotlin.collections.set * and class loading problems. */ abstract class ManagedStrategy( - private val testClass: Class<*>, + private val testClass: TestClass, scenario: ExecutionScenario, private val verifier: Verifier, - private val validationFunctions: List, - private val stateRepresentationFunction: Method?, + private val validationFunctions: List, + private val stateRepresentationFunction: StateRepresentationFunction?, private val testCfg: ManagedCTestConfiguration ) : Strategy(scenario), Closeable { // The number of parallel threads. @@ -118,7 +117,7 @@ abstract class ManagedStrategy( ManagedStrategyRunner(this, testClass, validationFunctions, stateRepresentationFunction, testCfg.timeoutMs, UseClocks.ALWAYS) private fun initializeManagedState() { - ManagedStrategyStateHolder.setState(runner.classLoader, this, testClass) + ManagedStrategyStateHolder.setState(runner.classLoader as ClassLoader, this, testClass) } override fun createTransformer(cv: ClassVisitor): ClassVisitor = ManagedStrategyTransformer( @@ -175,7 +174,7 @@ abstract class ManagedStrategy( ignoredSectionDepth.fill(0) callStackTrace.forEach { it.clear() } suspendedFunctionsStack.forEach { it.clear() } - ManagedStrategyStateHolder.resetState(runner.classLoader, testClass) + ManagedStrategyStateHolder.resetState(runner.classLoader as ClassLoader, testClass) } // == BASIC STRATEGY METHODS == @@ -223,11 +222,11 @@ abstract class ManagedStrategy( val sameResults = loggedResults !is CompletedInvocationResult || failingResult !is CompletedInvocationResult || loggedResults.results == failingResult.results check(sameResultTypes && sameResults) { StringBuilder().apply { - appendln("Non-determinism found. Probably caused by non-deterministic code (WeakHashMap, Object.hashCode, etc).") - appendln("== Reporting the first execution without execution trace ==") - appendln(failingResult.asLincheckFailureWithoutTrace()) - appendln("== Reporting the second execution ==") - appendln(loggedResults.toLincheckFailure(scenario, Trace(traceCollector!!.trace, testCfg.verboseTrace)).toString()) + appendLine("Non-determinism found. Probably caused by non-deterministic code (WeakHashMap, Object.hashCode, etc).") + appendLine("== Reporting the first execution without execution trace ==") + appendLine(failingResult.asLincheckFailureWithoutTrace()) + appendLine("== Reporting the second execution ==") + appendLine(loggedResults.toLincheckFailure(scenario, Trace(traceCollector!!.trace, testCfg.verboseTrace)).toString()) }.toString() } return Trace(traceCollector!!.trace, testCfg.verboseTrace) @@ -733,9 +732,9 @@ abstract class ManagedStrategy( * to the strategy so that it can known about some required events. */ private class ManagedStrategyRunner( - private val managedStrategy: ManagedStrategy, testClass: Class<*>, validationFunctions: List, - stateRepresentationMethod: Method?, timeoutMs: Long, useClocks: UseClocks -) : ParallelThreadsRunner(managedStrategy, testClass, validationFunctions, stateRepresentationMethod, timeoutMs, useClocks) { + private val managedStrategy: ManagedStrategy, testClass: TestClass, validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, timeoutMs: Long, useClocks: UseClocks +) : ParallelThreadsRunner(managedStrategy, testClass, validationFunctions, stateRepresentationFunction, timeoutMs, useClocks) { override fun onStart(iThread: Int) { super.onStart(iThread) managedStrategy.onStart(iThread) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategyStateHolder.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategyStateHolder.kt index 3442d4d42..ac1803f32 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategyStateHolder.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategyStateHolder.kt @@ -41,7 +41,7 @@ internal object ManagedStrategyStateHolder { /** * Sets the strategy and its initial state for the specified class loader. */ - fun setState(loader: ClassLoader, strategy: ManagedStrategy?, testClass: Class) { + fun setState(loader: ClassLoader, strategy: ManagedStrategy?, testClass: TestClass) { try { val clazz = loader.loadClass(ManagedStrategyStateHolder::class.java.canonicalName) clazz.getField("strategy")[null] = strategy @@ -57,17 +57,17 @@ internal object ManagedStrategyStateHolder { /** * Prepare the state for the specified class loader for the next invocation. */ - fun resetState(loader: ClassLoader, testClass: Class) { + fun resetState(loader: ClassLoader, testClass: TestClass) { try { val clazz = loader.loadClass(ManagedStrategyStateHolder::class.java.canonicalName) - clazz.getMethod("resetStateImpl", Class::class.java).invoke(null, testClass) + clazz.getMethod("resetStateImpl", TestClass::class.java).invoke(null, testClass) } catch (e: Exception) { throw IllegalStateException("Cannot set state to ManagedStateHolder", e) } } @JvmStatic - fun resetStateImpl(testClass: Class) { + fun resetStateImpl(testClass: TestClass) { random!!.setSeed(INITIAL_SEED) objectManager = ObjectManager(testClass) } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectManager.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectManager.kt index e0dbd9967..9d08a60c8 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectManager.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectManager.kt @@ -21,6 +21,7 @@ */ package org.jetbrains.kotlinx.lincheck.strategy.managed +import org.jetbrains.kotlinx.lincheck.* import java.util.* /** @@ -34,7 +35,7 @@ import java.util.* * associated with the corresponding field names, so that in the trace users see * the field names instead of something like `AtomicInteger@100500`. */ -internal class ObjectManager(private val testClass: Class) { +internal class ObjectManager(private val testClass: TestClass) { // For each local object store all objects that depend on it (e.g, are referenced by it). // Non-local objects are not presented in this map. private val localObjects = IdentityHashMap>() diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt index 61dfacba9..0ba3cd885 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt @@ -28,8 +28,6 @@ import java.math.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* -data class Trace(val trace: List, val verboseTrace: Boolean) - /** * Essentially, a trace is a list of trace points, which represent * interleaving events, such as code location passing or thread switches, diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt index 492fe9af3..ea3c56133 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt @@ -22,10 +22,8 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult -import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario -import org.jetbrains.kotlinx.lincheck.execution.parallelResults -import org.jetbrains.kotlinx.lincheck.printInColumnsCustom +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.strategy.* import java.util.* import kotlin.math.min diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt index b2ccdefa9..0d504e179 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt @@ -21,25 +21,25 @@ */ package org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* import org.jetbrains.kotlinx.lincheck.verifier.* -import java.lang.reflect.* /** * Configuration for [random search][ModelCheckingStrategy] strategy. */ -class ModelCheckingCTestConfiguration(testClass: Class<*>, iterations: Int, threads: Int, actorsPerThread: Int, actorsBefore: Int, - actorsAfter: Int, generatorClass: Class, verifierClass: Class, - checkObstructionFreedom: Boolean, hangingDetectionThreshold: Int, invocationsPerIteration: Int, - guarantees: List, requireStateEquivalenceCheck: Boolean, minimizeFailedScenario: Boolean, - sequentialSpecification: Class<*>, timeoutMs: Long, eliminateLocalObjects: Boolean, verboseTrace: Boolean, - customScenarios: List -) : ManagedCTestConfiguration(testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, generatorClass, verifierClass, +class ModelCheckingCTestConfiguration(testClass: TestClass, iterations: Int, threads: Int, actorsPerThread: Int, actorsBefore: Int, + actorsAfter: Int, executionGenerator: (testConfiguration: CTestConfiguration, testStructure: CTestStructure) -> ExecutionGenerator, + verifierClass: (sequentialSpecification: SequentialSpecification<*>) -> Verifier, checkObstructionFreedom: Boolean, hangingDetectionThreshold: Int, + invocationsPerIteration: Int, guarantees: List, requireStateEquivalenceCheck: Boolean, + minimizeFailedScenario: Boolean, sequentialSpecification: SequentialSpecification<*>, timeoutMs: Long, + eliminateLocalObjects: Boolean, verboseTrace: Boolean, customScenarios: List +) : ManagedCTestConfiguration(testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, executionGenerator, verifierClass, checkObstructionFreedom, hangingDetectionThreshold, invocationsPerIteration, guarantees, requireStateEquivalenceCheck, minimizeFailedScenario, sequentialSpecification, timeoutMs, eliminateLocalObjects, verboseTrace, customScenarios) { - override fun createStrategy(testClass: Class<*>, scenario: ExecutionScenario, validationFunctions: List, - stateRepresentationMethod: Method?, verifier: Verifier): Strategy - = ModelCheckingStrategy(this, testClass, scenario, validationFunctions, stateRepresentationMethod, verifier) + override fun createStrategy(testClass: TestClass, scenario: ExecutionScenario, validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, verifier: Verifier): Strategy + = ModelCheckingStrategy(this, testClass, scenario, validationFunctions, stateRepresentationFunction, verifier) } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingOptions.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingOptions.kt index 26ee21bf3..9ef4934f3 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingOptions.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingOptions.kt @@ -28,9 +28,9 @@ import org.jetbrains.kotlinx.lincheck.strategy.managed.* * Options for [model checking][ModelCheckingStrategy] strategy. */ class ModelCheckingOptions : ManagedOptions() { - override fun createTestConfigurations(testClass: Class<*>): ModelCheckingCTestConfiguration { + override fun createTestConfigurations(testClass: TestClass): ModelCheckingCTestConfiguration { return ModelCheckingCTestConfiguration(testClass, iterations, threads, actorsPerThread, actorsBefore, actorsAfter, - executionGenerator, verifier, checkObstructionFreedom, hangingDetectionThreshold, invocationsPerIteration, + executionGeneratorGenerator, verifierGenerator, checkObstructionFreedom, hangingDetectionThreshold, invocationsPerIteration, guarantees, requireStateEquivalenceImplementationCheck, minimizeFailedScenario, chooseSequentialSpecification(sequentialSpecification, testClass), timeoutMs, eliminateLocalObjects, verboseTrace, customScenarios) diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt index 9da818ce9..d50473030 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt @@ -21,6 +21,7 @@ */ package org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* @@ -44,12 +45,12 @@ import kotlin.random.* * than the number of all possible interleavings on the current depth level. */ internal class ModelCheckingStrategy( - testCfg: ModelCheckingCTestConfiguration, - testClass: Class<*>, - scenario: ExecutionScenario, - validationFunctions: List, - stateRepresentation: Method?, - verifier: Verifier + testCfg: ModelCheckingCTestConfiguration, + testClass: TestClass, + scenario: ExecutionScenario, + validationFunctions: List, + stateRepresentation: StateRepresentationFunction?, + verifier: Verifier ) : ManagedStrategy(testClass, scenario, verifier, validationFunctions, stateRepresentation, testCfg) { // The number of invocations that the strategy is eligible to use to search for an incorrect execution. private val maxInvocations = testCfg.invocationsPerIteration diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTest.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTest.java deleted file mode 100644 index 509fccde8..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.strategy.stress; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import org.jetbrains.kotlinx.lincheck.CTestConfiguration; -import org.jetbrains.kotlinx.lincheck.annotations.Operation; -import org.jetbrains.kotlinx.lincheck.execution.ExecutionGenerator; -import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario; -import org.jetbrains.kotlinx.lincheck.execution.RandomExecutionGenerator; -import org.jetbrains.kotlinx.lincheck.verifier.DummySequentialSpecification; -import org.jetbrains.kotlinx.lincheck.verifier.linearizability.LinearizabilityVerifier; -import org.jetbrains.kotlinx.lincheck.verifier.Verifier; - -import java.lang.annotation.*; - -/** - * This annotation configures concurrent test using stress strategy. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Repeatable(StressCTest.StressCTests.class) -@Inherited -public @interface StressCTest { - /** - * The number of different test scenarios to be executed - */ - int iterations() default CTestConfiguration.DEFAULT_ITERATIONS; - - /** - * Run each test scenario {@code invocations} times. - */ - int invocationsPerIteration() default StressCTestConfiguration.DEFAULT_INVOCATIONS; - - /** - * Use the specified number of threads for the parallel part of an execution. - *

- * Note, that the the actual number of threads can be less due to some restrictions - * like {@link Operation#runOnce()}. - * - * @see ExecutionScenario#parallelExecution - */ - int threads() default CTestConfiguration.DEFAULT_THREADS; - - /** - * Generate the specified number of operations for each thread of the parallel part of an execution. - *

- * Note, that the the actual number of operations can be less due to some restrictions - * like {@link Operation#runOnce()}. - * - * @see ExecutionScenario#parallelExecution - */ - int actorsPerThread() default CTestConfiguration.DEFAULT_ACTORS_PER_THREAD; - - /** - * Generate the specified number of operation for the initial sequential part of an execution. - *

- * Note, that the the actual number of operations can be less due to some restrictions - * like {@link Operation#runOnce()}. - * - * @see ExecutionScenario#initExecution - */ - int actorsBefore() default CTestConfiguration.DEFAULT_ACTORS_BEFORE; - - /** - * Generate the specified number of operation for the last sequential part of an execution. - *

- * Note, that the the actual number of operations can be less due to some restrictions - * like {@link Operation#runOnce()}. - * - * @see ExecutionScenario#postExecution - */ - int actorsAfter() default CTestConfiguration.DEFAULT_ACTORS_AFTER; - - /** - * Use the specified execution generator. - */ - Class generator() default RandomExecutionGenerator.class; - - /** - * Use the specified verifier. - */ - Class verifier() default LinearizabilityVerifier.class; - - /** - * Require correctness check of test instance state equivalency relation, which is defined by the user. - * Essentially, it checks whether two new instances of the test class are equal. - * If the check fails, an {@link IllegalStateException} is thrown. - */ - boolean requireStateEquivalenceImplCheck() default false; - - /** - * If this feature is enabled and an invalid interleaving has been found, - * *lincheck* tries to minimize the corresponding scenario in order to - * construct a smaller one so that the test fails on it as well. - * Enabled by default. - */ - boolean minimizeFailedScenario() default true; - - /** - * The specified class defines the sequential behavior of the testing data structure; - * it is used by {@link Verifier} to build a labeled transition system, - * and should have the same methods as the testing data structure. - * - * By default, the provided concurrent implementation is used in a sequential way. - */ - Class sequentialSpecification() default DummySequentialSpecification.class; - - /** - * Holder annotation for {@link StressCTest}. - */ - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - @Inherited - @interface StressCTests { - StressCTest[] value(); - } -} - diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTest.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTest.kt new file mode 100644 index 000000000..cfb09d9dd --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressCTest.kt @@ -0,0 +1,129 @@ +/* +* #%L +* Lincheck +* %% +* Copyright (C) 2015 - 2018 Devexperts, LLC +* %% +* 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.strategy.stress + +import org.jetbrains.kotlinx.lincheck.CTestConfiguration +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionGenerator +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.execution.RandomExecutionGenerator +import org.jetbrains.kotlinx.lincheck.verifier.DummySequentialSpecification +import org.jetbrains.kotlinx.lincheck.verifier.Verifier +import org.jetbrains.kotlinx.lincheck.verifier.linearizability.LinearizabilityVerifier +import java.lang.annotation.Inherited +import java.lang.annotation.Repeatable +import kotlin.reflect.KClass + +/** + * This annotation configures concurrent test using stress strategy. + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +@Repeatable(StressCTest.StressCTests::class) +@Inherited +annotation class StressCTest( + /** + * The number of different test scenarios to be executed + */ + val iterations: Int = CTestConfiguration.DEFAULT_ITERATIONS, + /** + * Run each test scenario `invocations` times. + */ + val invocationsPerIteration: Int = StressCTestConfiguration.DEFAULT_INVOCATIONS, + /** + * Use the specified number of threads for the parallel part of an execution. + * + * + * Note, that the the actual number of threads can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.parallelExecution + */ + val threads: Int = CTestConfiguration.DEFAULT_THREADS, + /** + * Generate the specified number of operations for each thread of the parallel part of an execution. + * + * + * Note, that the the actual number of operations can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.parallelExecution + */ + val actorsPerThread: Int = CTestConfiguration.DEFAULT_ACTORS_PER_THREAD, + /** + * Generate the specified number of operation for the initial sequential part of an execution. + * + * + * Note, that the the actual number of operations can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.initExecution + */ + val actorsBefore: Int = CTestConfiguration.DEFAULT_ACTORS_BEFORE, + /** + * Generate the specified number of operation for the last sequential part of an execution. + * + * + * Note, that the the actual number of operations can be less due to some restrictions + * like [Operation.runOnce]. + * + * @see ExecutionScenario.postExecution + */ + val actorsAfter: Int = CTestConfiguration.DEFAULT_ACTORS_AFTER, + /** + * Use the specified execution generator. + */ + val generator: KClass = RandomExecutionGenerator::class, + /** + * Use the specified verifier. + */ + val verifier: KClass = LinearizabilityVerifier::class, + /** + * Require correctness check of test instance state equivalency relation, which is defined by the user. + * Essentially, it checks whether two new instances of the test class are equal. + * If the check fails, an [IllegalStateException] is thrown. + */ + val requireStateEquivalenceImplCheck: Boolean = false, + /** + * If this feature is enabled and an invalid interleaving has been found, + * *lincheck* tries to minimize the corresponding scenario in order to + * construct a smaller one so that the test fails on it as well. + * Enabled by default. + */ + val minimizeFailedScenario: Boolean = true, + /** + * The specified class defines the sequential behavior of the testing data structure; + * it is used by [Verifier] to build a labeled transition system, + * and should have the same methods as the testing data structure. + * + * By default, the provided concurrent implementation is used in a sequential way. + */ + val sequentialSpecification: KClass<*> = DummySequentialSpecification::class +) { + /** + * Holder annotation for [StressCTest]. + */ + @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) + @Inherited + annotation class StressCTests(vararg val value: StressCTest) +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt index 265c4a276..4f72f5132 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt @@ -21,22 +21,22 @@ */ package org.jetbrains.kotlinx.lincheck.strategy.stress +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.verifier.* -import java.lang.reflect.* -class StressStrategy( - testCfg: StressCTestConfiguration, - testClass: Class<*>, +actual class StressStrategy actual constructor( + private val testCfg: StressCTestConfiguration, + private val testClass: TestClass, scenario: ExecutionScenario, - validationFunctions: List, - stateRepresentationFunction: Method?, + private val validationFunctions: List, + private val stateRepresentationFunction: StateRepresentationFunction?, private val verifier: Verifier ) : Strategy(scenario) { private val invocations = testCfg.invocationsPerIteration - private val runner: Runner + private var runner: Runner init { runner = ParallelThreadsRunner( @@ -55,19 +55,23 @@ class StressStrategy( } } - override fun run(): LincheckFailure? { - runner.use { + actual override fun run(): LincheckFailure? { + try { // Run invocations for (invocation in 0 until invocations) { - when (val ir = runner.run()) { - is CompletedInvocationResult -> { - if (!verifier.verifyResults(scenario, ir.results)) - return IncorrectResultsFailure(scenario, ir.results) + runner.also { + when (val ir = runner.run()) { + is CompletedInvocationResult -> { + if (!verifier.verifyResults(scenario, ir.results)) + return IncorrectResultsFailure(scenario, ir.results) + } + else -> return ir.toLincheckFailure(scenario) } - else -> return ir.toLincheckFailure(scenario) } } return null + } finally { + runner.close() } } } \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/CachedVerifier.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/CachedVerifier.java deleted file mode 100644 index 25c56b962..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/CachedVerifier.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.verifier; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import org.jetbrains.kotlinx.lincheck.execution.*; - -import java.util.*; - -/** - * This verifier cached the already verified results in a hash table, - * and look into this hash table at first. In case of many invocations - * with the same scenario, this optimization improves the verification - * phase significantly. - */ -public abstract class CachedVerifier implements Verifier { - private final Map> previousResults = new WeakHashMap<>(); - - @Override - public boolean verifyResults(ExecutionScenario scenario, ExecutionResult results) { - boolean newResult = previousResults.computeIfAbsent(scenario, s -> new HashSet<>()).add(results); - if (!newResult) return true; - return verifyResultsImpl(scenario, results); - } - - public abstract boolean verifyResultsImpl(ExecutionScenario scenario, ExecutionResult results); -} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/Verifier.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/Verifier.java deleted file mode 100644 index 396baa09a..000000000 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/Verifier.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.jetbrains.kotlinx.lincheck.verifier; - -/* - * #%L - * Lincheck - * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC - * %% - * 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% - */ - -import org.jetbrains.kotlinx.lincheck.execution.*; -import org.jetbrains.kotlinx.lincheck.verifier.linearizability.LinearizabilityVerifier; - -/** - * Implementation of this interface verifies that execution is correct with respect to the algorithm contract. - * By default, it checks for linearizability (see {@link LinearizabilityVerifier}). - *

- * IMPORTANT! - * All implementations should have {@code (Class sequentialSpecification)} constructor, - * which takes the scenario to be tested and the correct sequential implementation of the testing data structure. - */ -public interface Verifier { - /** - * Verifies the specified results for correctness. - * Returns {@code true} if results are possible, {@code false} otherwise. - */ - boolean verifyResults(ExecutionScenario scenario, ExecutionResult results); - - /** - * Returns {@code true} when the state equivalence relation for the sequential specification - * is properly specified via {@link #equals(Object)} and {@link #hashCode()} methods. Returns - * `false` when two logically equal states do not satisfy the equals-hashCode contract. - */ - boolean checkStateEquivalenceImplementation(); -} diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/CompareSpeedTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/CompareSpeedTest.kt new file mode 100644 index 000000000..4e0ab72dc --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/CompareSpeedTest.kt @@ -0,0 +1,72 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.test + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.annotations.* +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.strategy.stress.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import org.junit.* +import java.util.concurrent.* +import java.util.concurrent.atomic.* + +class A { + private val sharedState: AtomicInteger = AtomicInteger(0) + + fun a() = synchronized(this) { + sharedState.incrementAndGet() + //printErr("a(), sharedState = ${sharedState.toString()}") + } + + fun b() = synchronized(this) { + sharedState.decrementAndGet() + //printErr("b(), sharedState = ${sharedState.toString()}") + } + + override fun equals(other: Any?): Boolean { + return this.sharedState.get() == (other as A).sharedState.get() + } + + override fun hashCode(): Int { + return this.sharedState.get() + } +} + +@StressCTest(iterations = 30, invocationsPerIteration = 10000, actorsBefore = 2, actorsPerThread = 2, actorsAfter = 2, threads = 3, minimizeFailedScenario = false) +class CompareSpeedTest: VerifierState() { + val state = A() + + override fun extractState(): Any { + return state + } + + @Operation + fun operation1() = state.a() + + @Operation + fun operation2() = state.b() + + @Test + fun test() { + LinChecker.check(CompareSpeedTest::class.java) + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/ExceptionAsResultTest.java b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/ExceptionAsResultTest.java index 1d85ba724..d0bb56d6b 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/ExceptionAsResultTest.java +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/ExceptionAsResultTest.java @@ -54,6 +54,7 @@ public void test() { fail("Should fail with AssertionError"); } catch (AssertionError e) { String m = e.getMessage(); + assertTrue(m.contains("Invalid execution results")); assertTrue(m.contains("IllegalStateException") || m.contains("IOException")); assertFalse(m.contains(MESSAGE)); } diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/FailedScenarioMinimizationTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/FailedScenarioMinimizationTest.kt index ae176311b..94a2fac33 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/FailedScenarioMinimizationTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/FailedScenarioMinimizationTest.kt @@ -22,9 +22,7 @@ package org.jetbrains.kotlinx.lincheck.test import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.annotations.Operation -import org.jetbrains.kotlinx.lincheck.paramgen.* import org.jetbrains.kotlinx.lincheck.strategy.stress.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.junit.* diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/PromptCancellationTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/PromptCancellationTest.kt index fd5f963a3..6f48cab24 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/PromptCancellationTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/PromptCancellationTest.kt @@ -32,7 +32,7 @@ import kotlin.reflect.* abstract class AbstractPromptCancellationTest( vararg expectedFailures: KClass, - val sequentialSpecification: KClass<*>? = null + val sequentialSpecification: SequentialSpecification<*>? = null ) : AbstractLincheckTest(*expectedFailures) { @Volatile private var returnResult = 0 @@ -80,7 +80,7 @@ abstract class AbstractPromptCancellationTest( actorsPerThread(1) actorsAfter(0) requireStateEquivalenceImplCheck(false) - sequentialSpecification?.let { sequentialSpecification(it.java) } + sequentialSpecification(this@AbstractPromptCancellationTest.sequentialSpecification) } } @@ -88,7 +88,7 @@ class CorrectPromptCancellationTest : AbstractPromptCancellationTest() class IncorrectPromptCancellationTest : AbstractPromptCancellationTest( IncorrectResultsFailure::class, - sequentialSpecification = IncorrectPromptCancellationSequential::class + sequentialSpecification = IncorrectPromptCancellationSequential::class.java ) class IncorrectPromptCancellationSequential { diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/AFUCallRepresentationTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/AFUCallRepresentationTest.kt index 88631ea8a..4cc2d825b 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/AFUCallRepresentationTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/AFUCallRepresentationTest.kt @@ -24,7 +24,6 @@ package org.jetbrains.kotlinx.lincheck.test.representation import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* -import org.jetbrains.kotlinx.lincheck.strategy.stress.* import org.jetbrains.kotlinx.lincheck.test.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.junit.* diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/StateRepresentationTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/StateRepresentationTest.kt index 99c76c2a9..93ecfc71d 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/StateRepresentationTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/StateRepresentationTest.kt @@ -21,7 +21,6 @@ */ package org.jetbrains.kotlinx.lincheck.test.representation -import kotlinx.atomicfu.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.annotations.StateRepresentation import org.jetbrains.kotlinx.lincheck.appendFailure diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/SuspendTraceReportingTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/SuspendTraceReportingTest.kt index 933484bdb..60caac2db 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/SuspendTraceReportingTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/SuspendTraceReportingTest.kt @@ -21,7 +21,6 @@ */ package org.jetbrains.kotlinx.lincheck.test.representation -import kotlinx.coroutines.* import kotlinx.coroutines.sync.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.Operation diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/ThreadDumpTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/ThreadDumpTest.kt index 295815654..9fb8681e6 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/ThreadDumpTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/representation/ThreadDumpTest.kt @@ -42,7 +42,7 @@ class ThreadDumpTest { .invocationTimeout(100) val failure = options.checkImpl(DeadlockOnSynchronizedTest::class.java) 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"} + check(failure.threadDump.dump.size == 2) { "thread dump for 2 threads expected, but for ${failure.threadDump.dump.size} threads was detected"} } } } diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/CancellationHandlingTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/CancellationHandlingTest.kt index fd5ef6bcc..23e2b349e 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/CancellationHandlingTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/CancellationHandlingTest.kt @@ -24,9 +24,7 @@ package org.jetbrains.kotlinx.lincheck.test.runner import kotlinx.coroutines.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.* -import org.jetbrains.kotlinx.lincheck.strategy.stress.* import org.jetbrains.kotlinx.lincheck.test.AbstractLincheckTest -import org.junit.* import java.util.concurrent.atomic.* class CancellationHandlingTest : AbstractLincheckTest() { diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/FixedActiveThreadsExecutorTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/FixedActiveThreadsExecutorTest.kt index 0749b2890..c078cbdc9 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/FixedActiveThreadsExecutorTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/FixedActiveThreadsExecutorTest.kt @@ -68,8 +68,8 @@ class FixedActiveThreadsExecutorTest { } try { executor.submitAndAwait(tasks, 200) - } catch (e: TimeoutException) { - return // TimeoutException is expected + } catch (e: LincheckTimeoutException) { + return@use // LincheckTimeoutException is expected } check(false) { "TimeoutException was expected" } } diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/ParallelThreadsRunnerExceptionTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/ParallelThreadsRunnerExceptionTest.kt index 2cdf28cb5..975cf2e71 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/ParallelThreadsRunnerExceptionTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/ParallelThreadsRunnerExceptionTest.kt @@ -101,7 +101,7 @@ class ParallelThreadsRunnerExceptionTest { parallel { thread { operation( - actor(susWithoutException), ExceptionResult.create(SuspendResumeScenarios.TestException::class.java, wasSuspended = true) + actor(susWithoutException), createExceptionResult(SuspendResumeScenarios.TestException::class.java, wasSuspended = true) ) } thread { @@ -110,7 +110,7 @@ class ParallelThreadsRunnerExceptionTest { } } ParallelThreadsRunner( - strategy = mockStrategy(scenario), testClass = testClass, validationFunctions = emptyList(), + strategy = mockStrategy(scenario), testClass = TestClass(testClass), validationFunctions = emptyList(), stateRepresentationFunction = null, useClocks = RANDOM, timeoutMs = DEFAULT_TIMEOUT_MS ).use { runner -> runner.initialize() @@ -126,7 +126,7 @@ class ParallelThreadsRunnerExceptionTest { parallel { thread { operation( - actor(susResumeThrow), ExceptionResult.create(SuspendResumeScenarios.TestException::class.java, wasSuspended = true) + actor(susResumeThrow), createExceptionResult(SuspendResumeScenarios.TestException::class.java, wasSuspended = true) ) } thread { @@ -135,7 +135,7 @@ class ParallelThreadsRunnerExceptionTest { } } ParallelThreadsRunner( - strategy = mockStrategy(scenario), testClass = testClass, validationFunctions = emptyList(), + strategy = mockStrategy(scenario), testClass = TestClass(testClass), validationFunctions = emptyList(), stateRepresentationFunction = null, useClocks = RANDOM, timeoutMs = DEFAULT_TIMEOUT_MS ).use { runner -> runner.initialize() @@ -149,12 +149,12 @@ class ParallelThreadsRunnerExceptionTest { val (scenario, expectedResults) = scenarioWithResults { parallel { thread { - operation(actor(susThrow), ExceptionResult.create(SuspendResumeScenarios.TestException::class.java)) + operation(actor(susThrow), createExceptionResult(SuspendResumeScenarios.TestException::class.java)) } } } ParallelThreadsRunner( - strategy = mockStrategy(scenario), testClass = testClass, validationFunctions = emptyList(), + strategy = mockStrategy(scenario), testClass = TestClass(testClass), validationFunctions = emptyList(), stateRepresentationFunction = null, useClocks = RANDOM, timeoutMs = DEFAULT_TIMEOUT_MS ).use { runner -> runner.initialize() diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/TestThreadExecutionHelperTest.java b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/TestThreadExecutionHelperTest.java index 0f1e933e6..6c5e46d8c 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/TestThreadExecutionHelperTest.java +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/runner/TestThreadExecutionHelperTest.java @@ -32,6 +32,7 @@ import static java.util.Arrays.*; import static java.util.Collections.*; +import static org.jetbrains.kotlinx.lincheck.UtilsKt.getKClassFromClass; public class TestThreadExecutionHelperTest { private Runner runner; @@ -45,7 +46,7 @@ public LincheckFailure run() { throw new UnsupportedOperationException(); } }; - runner = new Runner(strategy, ArrayDeque.class, emptyList(), null) { + runner = new Runner(strategy, new TestClass(ArrayDeque.class), emptyList(), null) { @Override public InvocationResult run() { throw new UnsupportedOperationException(); @@ -96,8 +97,8 @@ public void testActorExceptionHandling() throws Exception { asList( new Actor(ArrayDeque.class.getMethod("addLast", Object.class), asList(1)), new Actor(Queue.class.getMethod("remove"), emptyList()), - new Actor(Queue.class.getMethod("remove"), emptyList(), asList(NoSuchElementException.class)), - new Actor(Queue.class.getMethod("remove"), emptyList(), asList(Exception.class, NoSuchElementException.class)) + new Actor(Queue.class.getMethod("remove"), emptyList(), asList(getKClassFromClass(NoSuchElementException.class))), + new Actor(Queue.class.getMethod("remove"), emptyList(), asList(getKClassFromClass(Exception.class), getKClassFromClass(NoSuchElementException.class))) ), emptyList(), false); ex.testInstance = new ArrayDeque<>(); ex.results = new Result[4]; @@ -108,8 +109,8 @@ public void testActorExceptionHandling() throws Exception { Assert.assertArrayEquals(new Result[]{ VoidResult.INSTANCE, new ValueResult(1), - ExceptionResult.Companion.create(NoSuchElementException.class), - ExceptionResult.Companion.create(NoSuchElementException.class) + ResultKt.createExceptionResult(NoSuchElementException.class), + ResultKt.createExceptionResult(NoSuchElementException.class) }, ex.results); } } \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/modelchecking/ModelCheckingOptionsTest.java b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/modelchecking/ModelCheckingOptionsTest.java index d2a30aee8..eab528bbf 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/modelchecking/ModelCheckingOptionsTest.java +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/modelchecking/ModelCheckingOptionsTest.java @@ -23,6 +23,7 @@ import org.jetbrains.kotlinx.lincheck.LinChecker; import org.jetbrains.kotlinx.lincheck.LoggingLevel; +import org.jetbrains.kotlinx.lincheck.OptionsKt; import org.jetbrains.kotlinx.lincheck.annotations.Operation; import org.jetbrains.kotlinx.lincheck.execution.RandomExecutionGenerator; import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions; @@ -47,8 +48,8 @@ public void test() { ModelCheckingOptions opts = new ModelCheckingOptions() .iterations(10) .invocationsPerIteration(200) - .executionGenerator(RandomExecutionGenerator.class) - .verifier(LinearizabilityVerifier.class) +// .executionGenerator(RandomExecutionGenerator.class) +// .verifier(LinearizabilityVerifier.class) .threads(2) .actorsPerThread(3) .checkObstructionFreedom(true) @@ -57,6 +58,8 @@ public void test() { .addGuarantee(forClasses("java.util.WeakHashMap").allMethods().ignore()) .requireStateEquivalenceImplCheck(false) .minimizeFailedScenario(false); + OptionsKt.executionGenerator(opts, RandomExecutionGenerator.class); // TODO broken java api https://stackoverflow.com/questions/28294509/accessing-kotlin-extension-functions-from-java + OptionsKt.verifier(opts, LinearizabilityVerifier.class); LinChecker.check(ModelCheckingOptionsTest.class, opts); } } diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/stress/StressOptionsTest.java b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/stress/StressOptionsTest.java index 6b7b50099..7973a8dec 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/stress/StressOptionsTest.java +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/strategy/stress/StressOptionsTest.java @@ -44,13 +44,15 @@ public void test() { StressOptions opts = new StressOptions() .iterations(10) .invocationsPerIteration(200) - .executionGenerator(RandomExecutionGenerator.class) - .verifier(LinearizabilityVerifier.class) +// .executionGenerator(RandomExecutionGenerator.class) +// .verifier(LinearizabilityVerifier.class) .threads(2) .actorsPerThread(3) .logLevel(LoggingLevel.WARN) .requireStateEquivalenceImplCheck(false) .minimizeFailedScenario(false); + OptionsKt.executionGenerator(opts, RandomExecutionGenerator.class); // TODO broken java api https://stackoverflow.com/questions/28294509/accessing-kotlin-extension-functions-from-java + OptionsKt.verifier(opts, LinearizabilityVerifier.class); LinChecker.check(StressOptionsTest.class, opts); } } \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/CustomScenarioDSL.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/CustomScenarioDSL.kt index 849ec9eb9..5ae8e02e9 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/CustomScenarioDSL.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/CustomScenarioDSL.kt @@ -25,7 +25,7 @@ import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.execution.* import org.jetbrains.kotlinx.lincheck.verifier.* import java.lang.IllegalStateException -import kotlin.reflect.KFunction +import kotlin.reflect.* import kotlin.reflect.jvm.javaMethod /** @@ -58,7 +58,7 @@ fun verify( correct: Boolean ) { val (scenario, results) = scenarioWithResults(block) - val verifier = verifierClass.getConstructor(Class::class.java).newInstance(testClass) + val verifier = verifierClass.getConstructor(SequentialSpecification::class.java).newInstance(testClass) val res = verifier.verifyResults(scenario, results) assert(res == correct) } diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/ClocksTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/ClocksTest.kt index 99579f8bc..14ee1674d 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/ClocksTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/ClocksTest.kt @@ -67,11 +67,11 @@ class ClocksTestScenarioGenerator(testCfg: CTestConfiguration, testStructure: CT override fun nextExecution() = ExecutionScenario( emptyList(), listOf( - listOf( + listOf( Actor(method = ClocksTest::a.javaMethod!!, arguments = emptyList()), Actor(method = ClocksTest::b.javaMethod!!, arguments = emptyList()) ), - listOf( + listOf( Actor(method = ClocksTest::c.javaMethod!!, arguments = emptyList()), Actor(method = ClocksTest::d.javaMethod!!, arguments = emptyList()) ) diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/SkipListMapTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/SkipListMapTest.kt index 696604ce3..96b329766 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/SkipListMapTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck/test/verifier/linearizability/SkipListMapTest.kt @@ -21,11 +21,9 @@ */ package org.jetbrains.kotlinx.lincheck.test.verifier.linearizability -import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.paramgen.* import org.jetbrains.kotlinx.lincheck.test.* -import org.junit.* import java.util.concurrent.* @Param(name = "value", gen = IntGen::class, conf = "1:5") diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/Actor.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/Actor.kt new file mode 100644 index 000000000..da6b36cd1 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/Actor.kt @@ -0,0 +1,55 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import kotlin.reflect.KClass + +/** + * The actor entity describe the operation with its parameters + * which is executed during the testing. + * + * @see Operation + */ +actual class Actor( + val function: (Any, List) -> Any?, // (instance, arguments) -> result + val arguments: List, + val functionName: String = function.toString(), + actual val isSuspendable: Boolean = false, + actual val allowExtraSuspension: Boolean = false, + actual val promptCancellation: Boolean = false, + actual val cancelOnSuspension: Boolean = false, + actual val handledExceptions: List> = emptyList() +) { + + actual override fun toString(): String = + functionName + + arguments.joinToString(prefix = "(", postfix = ")", separator = ", ") { it.toString() } + + (if (cancelOnSuspension) " + " else "") + + (if (promptCancellation) "prompt_" else "") + + (if (cancelOnSuspension) "cancel" else "") + + actual fun finalize() { + arguments.forEach { if(it is Finalizable) it.finalize() } + } +} + +actual val Actor.isQuiescentConsistent: Boolean + get() = false // TODO check \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt new file mode 100644 index 000000000..9c3a9fbab --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt @@ -0,0 +1,778 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import kotlinx.cinterop.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.runner.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.strategy.stress.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.reflect.* + +typealias CCreator = CPointer CPointer<*>>> +typealias CDestructor = CPointer) -> Unit>> +typealias EqualsCFunction = CPointer, CPointer<*>) -> Boolean>> +typealias HashCodeCFunction = CPointer) -> Int>> +// write first argument represented as string to second argument preallocated memory. Third argument is second's argument size +typealias ToStringCFunction = CPointer, CPointer, Int) -> Unit>> + +internal fun applyToStringFunction(pointer: CPointer<*>, toStringFunction: ToStringCFunction, maxLen: Int = 500): String { + val buf = ByteArray(maxLen) + buf.usePinned { pinned -> + toStringFunction(pointer, pinned.addressOf(0), buf.size - 1) + } + return buf.toKString() +} + +internal class ObjectWithDestructorAndEqualsAndHashcodeAndToString(val obj: CPointer<*>, + val destructor: CDestructor, + val equals: EqualsCFunction, + val hashCode: HashCodeCFunction, + val toString: ToStringCFunction) : Finalizable { + + override fun equals(other: Any?): Boolean { + return if(other === this) { + true + } else if (other is ObjectWithDestructorAndEqualsAndHashcodeAndToString) { + equals.invoke(obj, other.obj) + } else { + false + } + } + + override fun hashCode(): Int { + return hashCode.invoke(obj) + } + + override fun toString(): String { + return applyToStringFunction(obj, toString) + } + + override fun finalize() { + // there are no destructors in Kotlin/Native :( https://youtrack.jetbrains.com/issue/KT-44191 + destructor.invoke(obj) + } +} + +internal class ParameterGeneratorArgument(val arg: CPointer<*>, + val destructor: CDestructor, + val toString: ToStringCFunction) : Finalizable { + override fun toString(): String { + return applyToStringFunction(arg, toString) + } + + override fun finalize() { + // there are no destructors in Kotlin/Native :( https://youtrack.jetbrains.com/issue/KT-44191 + destructor.invoke(arg) + } +} + +internal class ConcurrentInstance(val obj: CPointer<*>, val destructor: CDestructor) : Finalizable { + + override fun finalize() { + // there are no destructors in Kotlin/Native :( https://youtrack.jetbrains.com/issue/KT-44191 + destructor.invoke(obj) + } +} + +internal class SequentialSpecificationInstance(val obj: CPointer<*>, + val destructor: CDestructor, + val equalsFunction: EqualsCFunction, + val hashCodeFunction: HashCodeCFunction) : Finalizable { + override fun equals(other: Any?): Boolean { + return if (other === this) { + true + } else if (other is SequentialSpecificationInstance) { + equalsFunction.invoke(obj, other.obj) + } else { + false + } + } + + override fun hashCode(): Int { + return hashCodeFunction.invoke(obj) + } + + override fun finalize() { + // there are no destructors in Kotlin/Native :( https://youtrack.jetbrains.com/issue/KT-44191 + destructor.invoke(obj) + } +} + +fun throwKotlinValidationException(message: String) { + throw RuntimeException("Validation failure: $message") +} + +class NativeAPIStressConfiguration : LincheckStressConfiguration() { + private var initialStateCreator: CCreator? = null + private var initialStateDestructor: CDestructor? = null + private var sequentialSpecificationCreator: CCreator? = null + private var sequentialSpecificationDestructor: CDestructor? = null + private var sequentialSpecificationEquals: EqualsCFunction? = null + private var sequentialSpecificationHashCode: HashCodeCFunction? = null + private var initialStateSet: Boolean = false + + init { + // default configuration + iterations(10) + invocationsPerIteration(500) + } + + fun setupInvocationsPerIteration(count: Int) { + invocationsPerIteration(count) + } + + fun setupIterations(count: Int) { + iterations(count) + } + + fun setupThreads(count: Int) { + threads(count) + } + + fun setupActorsPerThread(count: Int) { + actorsPerThread(count) + } + + fun setupActorsBefore(count: Int) { + actorsBefore(count) + } + + fun setupActorsAfter(count: Int) { + actorsAfter(count) + } + + // executionGenerator + // verifier + fun setupRequireStateEquivalenceImplCheck(require: Boolean) { + requireStateEquivalenceImplCheck(require) + } + + fun setupMinimizeFailedScenario(minimizeFailedScenario: Boolean) { + minimizeFailedScenario(minimizeFailedScenario) + } + + fun disableVerifier() { + verifier { sequentialSpecification -> EpsilonVerifier(sequentialSpecification) } + } + + fun setupValidationFunction(function: CPointer) -> CPointer<*>>>) { + validationFunction({ + when (this) { + is ConcurrentInstance -> { + try { + function.invoke(this.obj) + } catch(e: RuntimeException) { + throw IllegalStateException(e) + } + } + else -> { + throw RuntimeException("Internal error. ValidationFunction has invoked not on ConcurrentInstance") + } + } + }) + } + + fun setupInitThreadFunction(function: CPointer Unit>>) { + initThreadFunction { function.invoke() } + } + + fun setupFinishThreadFunction(function: CPointer Unit>>) { + finishThreadFunction { function.invoke() } + } + + fun runNativeTest(printErrorToStderr: Boolean): String { + if (!initialStateSet) { + val errorMessage = "Please provide initialState, skipping..." + if (printErrorToStderr) printErr(errorMessage) + return errorMessage + } + if (sequentialSpecificationCreator == null) { + val errorMessage = "Please provide sequentialSpecification, skipping..." + if (printErrorToStderr) printErr(errorMessage) + return errorMessage + } + if (initialStateCreator != null) { + // different initialState and sequentialSpecification + initialState { + ConcurrentInstance(initialStateCreator!!.invoke(), initialStateDestructor!!) + } + } else { + initialState { + ConcurrentInstance(sequentialSpecificationCreator!!.invoke(), sequentialSpecificationDestructor!!) + } + } + sequentialSpecification( + SequentialSpecification { + SequentialSpecificationInstance(sequentialSpecificationCreator!!.invoke(), sequentialSpecificationDestructor!!, sequentialSpecificationEquals!!, sequentialSpecificationHashCode!!) + } + ) + try { + runTest() + } catch (e: LincheckAssertionError) { + val errorMessage = e.toString() + if (printErrorToStderr) printErr(errorMessage) + return errorMessage + } + return "" + } + + fun setupInitialState( + initialStateCreator: CCreator, + initialStateDestructor: CDestructor + ) { + initialStateSet = true + this.initialStateCreator = initialStateCreator + this.initialStateDestructor = initialStateDestructor + } + + fun setupInitialStateAndSequentialSpecification( + initialStateCreator: CCreator, + initialStateDestructor: CDestructor, + equals: EqualsCFunction, + hashCode: HashCodeCFunction + ) { + initialStateSet = true + this.initialStateCreator = null + this.initialStateDestructor = null + this.sequentialSpecificationCreator = initialStateCreator + this.sequentialSpecificationDestructor = initialStateDestructor + this.sequentialSpecificationEquals = equals + this.sequentialSpecificationHashCode = hashCode + } + + fun setupSequentialSpecification( + sequentialSpecificationCreator: CCreator, + initialStateDestructor: CDestructor, + equals: EqualsCFunction, + hashCode: HashCodeCFunction + ) { + this.sequentialSpecificationCreator = sequentialSpecificationCreator + this.sequentialSpecificationDestructor = initialStateDestructor + this.sequentialSpecificationEquals = equals + this.sequentialSpecificationHashCode = hashCode + } + + fun setupOperation1( + op: CPointer) -> CPointer<*>>>, + seq_spec: CPointer) -> CPointer<*>>>, + result_destructor: CDestructor, + result_equals: EqualsCFunction, + result_hashCode: HashCodeCFunction, + result_toString: ToStringCFunction, + operationName: String, + nonParallelGroupName: String? = null, + useOnce: Boolean = false, + ) = apply { + val actorGenerator = ActorGenerator( + function = { instance, _ -> + when (instance) { + is ConcurrentInstance -> { + ObjectWithDestructorAndEqualsAndHashcodeAndToString(op.invoke(instance.obj), result_destructor, result_equals, result_hashCode, result_toString) + } + is SequentialSpecificationInstance -> { + ObjectWithDestructorAndEqualsAndHashcodeAndToString(seq_spec.invoke(instance.obj), result_destructor, result_equals, result_hashCode, result_toString) + } + else -> { + throw RuntimeException("Internal error. Instance has not expected type") + } + } + }, + parameterGenerators = emptyList(), + functionName = operationName, + useOnce = useOnce, + isSuspendable = false, + handledExceptions = emptyList() + ) + actorGenerators.add(actorGenerator) + addToOperationGroups(nonParallelGroupName, actorGenerator) + } + + fun setupOperation2( + arg1_gen_initial_state: CCreator, + arg1_gen_generate: CPointer) -> CPointer<*>>>, + arg1_toString: ToStringCFunction, + arg1_destructor: CDestructor, + op: CPointer, CPointer<*>) -> CPointer<*>>>, + seq_spec: CPointer, CPointer<*>) -> CPointer<*>>>, + result_destructor: CDestructor, + result_equals: EqualsCFunction, + result_hashCode: HashCodeCFunction, + result_toString: ToStringCFunction, + operationName: String, + nonParallelGroupName: String? = null, + useOnce: Boolean = false, + ) = apply { + val arg1_paramgen = object : ParameterGenerator { + val state = arg1_gen_initial_state.invoke() + override fun generate(): ParameterGeneratorArgument { + return ParameterGeneratorArgument(arg1_gen_generate.invoke(state), arg1_destructor, arg1_toString) + } + } + val actorGenerator = ActorGenerator( + function = { instance, arguments -> + when (instance) { + is ConcurrentInstance -> { + ObjectWithDestructorAndEqualsAndHashcodeAndToString(op.invoke(instance.obj, (arguments[0] as ParameterGeneratorArgument).arg), result_destructor, result_equals, result_hashCode, result_toString) + } + is SequentialSpecificationInstance -> { + ObjectWithDestructorAndEqualsAndHashcodeAndToString(seq_spec.invoke(instance.obj, (arguments[0] as ParameterGeneratorArgument).arg), result_destructor, result_equals, result_hashCode, result_toString) + } + else -> { + throw RuntimeException("Internal error. Instance has not expected type") + } + } + }, + parameterGenerators = listOf(arg1_paramgen), + functionName = operationName, + useOnce = useOnce, + isSuspendable = false, + handledExceptions = emptyList() + ) + actorGenerators.add(actorGenerator) + addToOperationGroups(nonParallelGroupName, actorGenerator) + } + + fun setupOperation3( + arg1_gen_initial_state: CCreator, + arg1_gen_generate: CPointer) -> CPointer<*>>>, + arg1_toString: ToStringCFunction, + arg1_destructor: CDestructor, + arg2_gen_initial_state: CCreator, + arg2_gen_generate: CPointer) -> CPointer<*>>>, + arg2_toString: ToStringCFunction, + arg2_destructor: CDestructor, + op: CPointer, CPointer<*>, CPointer<*>) -> CPointer<*>>>, + seq_spec: CPointer, CPointer<*>, CPointer<*>) -> CPointer<*>>>, + result_destructor: CDestructor, + result_equals: EqualsCFunction, + result_hashCode: HashCodeCFunction, + result_toString: ToStringCFunction, + operationName: String, + nonParallelGroupName: String? = null, + useOnce: Boolean = false, + ) = apply { + val arg1Paramgen = object : ParameterGenerator { + val state = arg1_gen_initial_state.invoke() + override fun generate(): ParameterGeneratorArgument { + return ParameterGeneratorArgument(arg1_gen_generate.invoke(state), arg1_destructor, arg1_toString) + } + } + val arg2Paramgen = object : ParameterGenerator { + val state = arg2_gen_initial_state.invoke() + override fun generate(): ParameterGeneratorArgument { + return ParameterGeneratorArgument(arg2_gen_generate.invoke(state), arg2_destructor, arg2_toString) + } + } + val actorGenerator = ActorGenerator( + function = { instance, arguments -> + when (instance) { + is ConcurrentInstance -> { + ObjectWithDestructorAndEqualsAndHashcodeAndToString(op.invoke(instance.obj, (arguments[0] as ParameterGeneratorArgument).arg, (arguments[1] as ParameterGeneratorArgument).arg), result_destructor, result_equals, result_hashCode, result_toString) + } + is SequentialSpecificationInstance -> { + ObjectWithDestructorAndEqualsAndHashcodeAndToString(seq_spec.invoke(instance.obj, (arguments[0] as ParameterGeneratorArgument).arg, (arguments[1] as ParameterGeneratorArgument).arg), result_destructor, result_equals, result_hashCode, result_toString) + } + else -> { + throw RuntimeException("Internal error. Instance has not expected type") + } + } + }, + parameterGenerators = listOf(arg1Paramgen, arg2Paramgen), + functionName = operationName, + useOnce = useOnce, + isSuspendable = false, + handledExceptions = emptyList() + ) + actorGenerators.add(actorGenerator) + addToOperationGroups(nonParallelGroupName, actorGenerator) + } +} + +// TODO StressOptions methods cast this class to StressOptions +open class LincheckStressConfiguration(protected val testName: String = "") : StressOptions() { + /* + invocationsPerIteration + iterations + threads + actorsPerThread + actorsBefore + actorsAfter + executionGenerator(executionGenerator: (testConfiguration: CTestConfiguration, testStructure: CTestStructure) -> ExecutionGenerator) + verifier(verifier: (sequentialSpecification: SequentialSpecification<*>) -> Verifier) + requireStateEquivalenceImplCheck + minimizeFailedScenario + logLevel(logLevel: LoggingLevel) + sequentialSpecification(clazz: SequentialSpecification<*>?) + */ + protected var testClass: TestClass? = null + protected var actorGenerators = mutableListOf() + protected var operationGroups = mutableMapOf() + protected var validationFunctions = mutableListOf() + protected var stateRepresentationFunction: StateRepresentationFunction? = null + + init { + // override invocationsPerIteration + invocationsPerIteration(500) + } + + protected fun getTestClass(): TestClass { + return testClass ?: throw IllegalArgumentException("initialState should be specified") + } + + protected fun getTestStructure(): CTestStructure { + return CTestStructure( + actorGenerators, + operationGroups.values.toList(), + validationFunctions, + stateRepresentationFunction + ) + } + + fun runTest() { + val failure = checkImpl() ?: return + throw LincheckAssertionError(failure) + } + + fun checkImpl(): LincheckFailure? { + //printErr("iterations: $iterations, invocations: $invocationsPerIteration") + if (invocationsPerIteration > 500) { + printErr("WARNING invocations count is bigger than 500, that may lead to crash") // TODO remove when bug with GC will be fixed + } + val result = LinChecker(getTestClass(), getTestStructure(), this as StressOptions).checkImpl() + if (testName.isNotEmpty()) { + println("Finished test $testName") + } + return result + } + + // =========================== Constructor + + fun initialState( + state: () -> Instance + ) = apply { + testClass = TestClass(state) + } + + // =========================== Operation + + protected fun addToOperationGroups(nonParallelGroupName: String?, actorGenerator: ActorGenerator) { + nonParallelGroupName?.let { + if (!operationGroups.containsKey(nonParallelGroupName)) { + operationGroups[nonParallelGroupName] = OperationGroup(nonParallelGroupName, true) + } + operationGroups[nonParallelGroupName]!!.actors.add(actorGenerator) + } + } + + fun operation( + pGens: List>, + op: Instance.(List) -> R, + name: String = op.toString(), + handleExceptionsAsResult: List> = emptyList(), + nonParallelGroupName: String? = null, + useOnce: Boolean = false, + isSuspendable: Boolean = false + ) = apply { + val actorGenerator = ActorGenerator( + function = { instance, arguments -> + instance as Instance // check that operation can be applied to instance + instance.op(arguments) + }, + parameterGenerators = pGens, + functionName = name, + useOnce = useOnce, + isSuspendable = isSuspendable, + handledExceptions = handleExceptionsAsResult + ) + actorGenerators.add(actorGenerator) + addToOperationGroups(nonParallelGroupName, actorGenerator) + } + + fun operation( + op: Instance.() -> R, + name: String = op.toString(), + handleExceptionsAsResult: List> = emptyList(), + nonParallelGroupName: String? = null, + useOnce: Boolean = false, + isSuspendable: Boolean = false + ) = apply { + val actorGenerator = ActorGenerator( + function = { instance, arguments -> + instance as Instance // check that operation can be applied to instance + instance.op() + }, + parameterGenerators = listOf(), + functionName = name, + useOnce = useOnce, + isSuspendable = isSuspendable, + handledExceptions = handleExceptionsAsResult + ) + actorGenerators.add(actorGenerator) + addToOperationGroups(nonParallelGroupName, actorGenerator) + } + + fun operation( + p1Gen: ParameterGenerator, + op: Instance.(p1: P1) -> R, + name: String = op.toString(), + handleExceptionsAsResult: List> = emptyList(), + nonParallelGroupName: String? = null, + useOnce: Boolean = false, + isSuspendable: Boolean = false + ) = apply { + val actorGenerator = ActorGenerator( + function = { instance, arguments -> + instance as Instance // check that operation can be applied to instance + instance.op(arguments[0] as P1) // extract arguments and cast to type + }, + parameterGenerators = listOf(p1Gen), + functionName = name, + useOnce = useOnce, + isSuspendable = isSuspendable, + handledExceptions = handleExceptionsAsResult + ) + actorGenerators.add(actorGenerator) + addToOperationGroups(nonParallelGroupName, actorGenerator) + } + + fun operation( + p1Gen: ParameterGenerator, + p2Gen: ParameterGenerator, + op: Instance.(p1: P1, p2: P2) -> R, + name: String = op.toString(), + handleExceptionsAsResult: List> = emptyList(), + nonParallelGroupName: String? = null, + useOnce: Boolean = false, + isSuspendable: Boolean = false + ) = apply { + val actorGenerator = ActorGenerator( + function = { instance, arguments -> + instance as Instance // check that operation can be applied to instance + instance.op(arguments[0] as P1, arguments[1] as P2) // extract arguments and cast to type + }, + parameterGenerators = listOf(p1Gen, p2Gen), + functionName = name, + useOnce = useOnce, + isSuspendable = isSuspendable, + handledExceptions = handleExceptionsAsResult + ) + actorGenerators.add(actorGenerator) + addToOperationGroups(nonParallelGroupName, actorGenerator) + } + + // ============================= Validation Function + + fun validationFunction( + validate: Instance.() -> Unit, + name: String = validate.toString() + ) = apply { + validationFunctions.add(ValidationFunction({ instance -> + instance as Instance // check that operation can be applied to instance + instance.validate() + }, name)) + } + + // ============================= State Representation Function + + fun stateRepresentation( + state: Instance.() -> String + ) = apply { + stateRepresentationFunction = StateRepresentationFunction { instance -> + instance as Instance // check that operation can be applied to instance + instance.state() + } + } +} + +/** + * This class runs concurrent tests. + */ +class LinChecker(private val testClass: TestClass, private val testStructure: CTestStructure, options: Options<*, *>) { + private val testConfigurations: List + private val reporter: Reporter + + init { + val logLevel = options?.logLevel ?: DEFAULT_LOG_LEVEL + reporter = Reporter(logLevel) + testConfigurations = listOf(options.createTestConfigurations(testClass)) + } + + /** + * @throws LincheckAssertionError if the testing data structure is incorrect. + */ + fun check() { + val failure = checkImpl() ?: return + throw LincheckAssertionError(failure) + } + + /** + * @return TestReport with information about concurrent test run. + */ + internal fun checkImpl(): LincheckFailure? { + check(testConfigurations.isNotEmpty()) { "No Lincheck test configuration to run" } + for (testCfg in testConfigurations) { + val failure = testCfg.checkImpl() + if (failure != null) return failure + } + return null + } + + private fun CTestConfiguration.checkImpl(): LincheckFailure? { + val exGen = createExecutionGenerator() + val REPORT_PERIOD_MS = 1000 + var prevReport = currentTimeMillis() - REPORT_PERIOD_MS + repeat(iterations) { i -> + val verifier = createVerifier() // Could be created once, but created on every iteration to save memory and finalize scenario + // some magic computations for beautiful values + if (currentTimeMillis() - prevReport >= REPORT_PERIOD_MS) { + println("${i + 1}/$iterations") + prevReport = currentTimeMillis() + } + val scenario = exGen.nextExecution() + scenario.validate() + reporter.logIteration(i + 1, iterations, scenario) + val failure = scenario.run(this, verifier) + if (failure != null) { + val minimizedFailedIteration = if (!minimizeFailedScenario) failure + else failure.minimize(this, verifier) + reporter.logFailedIteration(minimizedFailedIteration) + return minimizedFailedIteration + } + scenario.finalize() + } + return null + } + + // Tries to minimize the specified failing scenario to make the error easier to understand. + // The algorithm is greedy: it tries to remove one actor from the scenario and checks + // whether a test with the modified one fails with error as well. If it fails, + // then the scenario has been successfully minimized, and the algorithm tries to minimize it again, recursively. + // Otherwise, if no actor can be removed so that the generated test fails, the minimization is completed. + // Thus, the algorithm works in the linear time of the total number of actors. + private fun LincheckFailure.minimize(testCfg: CTestConfiguration, verifier: Verifier): LincheckFailure { + reporter.logScenarioMinimization(scenario) + val parallelExecution = scenario.parallelExecution.map { it.toMutableList() }.toMutableList() + val initExecution = scenario.initExecution.toMutableList() + val postExecution = scenario.postExecution.toMutableList() + for (i in scenario.parallelExecution.indices) { + for (j in scenario.parallelExecution[i].indices) { + val newParallelExecution = parallelExecution.map { it.toMutableList() }.toMutableList() + newParallelExecution[i].removeAt(j) + if (newParallelExecution[i].isEmpty()) newParallelExecution.removeAt(i) // remove empty thread + val newScenario = ExecutionScenario( + initExecution, + newParallelExecution, + postExecution + ) + val newFailedIteration = newScenario.tryMinimize(testCfg, verifier) + if (newFailedIteration != null) return newFailedIteration.minimize(testCfg, verifier) + } + } + for (i in scenario.initExecution.indices) { + val newInitExecution = initExecution.toMutableList() + newInitExecution.removeAt(i) + val newScenario = ExecutionScenario( + newInitExecution, + parallelExecution, + postExecution + ) + val newFailedIteration = newScenario.tryMinimize(testCfg, verifier) + if (newFailedIteration != null) return newFailedIteration.minimize(testCfg, verifier) + } + for (i in scenario.postExecution.indices) { + val newPostExecution = postExecution.toMutableList() + newPostExecution.removeAt(i) + val newScenario = ExecutionScenario( + initExecution, + parallelExecution, + newPostExecution + ) + val newFailedIteration = newScenario.tryMinimize(testCfg, verifier) + if (newFailedIteration != null) return newFailedIteration.minimize(testCfg, verifier) + } + return this + } + + private fun ExecutionScenario.tryMinimize(testCfg: CTestConfiguration, verifier: Verifier) = + if (isValid) run(testCfg, verifier) else null + + private fun ExecutionScenario.run(testCfg: CTestConfiguration, verifier: Verifier): LincheckFailure? = + testCfg.createStrategy( + testClass = testClass, + scenario = this, + validationFunctions = testStructure.validationFunctions, + stateRepresentationFunction = testStructure.stateRepresentation, + verifier = verifier + ).run() + + private val ExecutionScenario.isValid: Boolean + get() = !isParallelPartEmpty && + (!hasSuspendableActors() || (!hasSuspendableActorsInInitPart && !hasPostPartAndSuspendableActors)) + + private fun ExecutionScenario.validate() { + require(!isParallelPartEmpty) { + "The generated scenario has empty parallel part" + } + if (hasSuspendableActors()) { + require(!hasSuspendableActorsInInitPart) { + "The generated scenario for the test class with suspendable methods contains suspendable actors in initial part" + } + require(!hasPostPartAndSuspendableActors) { + "The generated scenario for the test class with suspendable methods has non-empty post part" + } + } + } + + private val ExecutionScenario.hasSuspendableActorsInInitPart + get() = + initExecution.any(Actor::isSuspendable) + private val ExecutionScenario.hasPostPartAndSuspendableActors + get() = + (parallelExecution.any { actors -> actors.any { it.isSuspendable } } && postExecution.size > 0) + private val ExecutionScenario.isParallelPartEmpty + get() = + parallelExecution.map { it.size }.sum() == 0 + + + private fun CTestConfiguration.createVerifier() = + verifierGenerator(this.sequentialSpecification).also { + if (requireStateEquivalenceImplCheck) it.checkStateEquivalenceImplementation() + } + + private fun CTestConfiguration.createExecutionGenerator() = + executionGenerator(this, testStructure) + + // This companion object is used for backwards compatibility. + companion object { + /** + * Runs the specified concurrent tests. If [options] is null, the provided on + * the testing class `@...CTest` annotations are used to specify the test parameters. + * + * @throws AssertionError if any of the tests fails. + */ + fun check(testClass: TestClass, testStructure: CTestStructure, options: Options<*, *>) { + LinChecker(testClass, testStructure, options).check() + } + } +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/NativeUtils.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/NativeUtils.kt new file mode 100644 index 000000000..f493c3a85 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/NativeUtils.kt @@ -0,0 +1,148 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +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.* +import platform.posix.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* +import kotlin.reflect.* + +actual class TestClass( + val create: () -> Any? +) { + actual fun createInstance(): Any = create() ?: throw IllegalArgumentException("Constructor should not return null") +} + +actual class SequentialSpecification (val function: () -> Any?) + +actual fun loadSequentialSpecification(sequentialSpecification: SequentialSpecification<*>): SequentialSpecification = sequentialSpecification as SequentialSpecification + +actual fun SequentialSpecification.getInitialState(): T = function() as T + +internal actual fun createLincheckResult(res: Any?, wasSuspended: Boolean): Result { + TODO("Not yet implemented") +} + +internal actual fun executeValidationFunction(instance: Any, validationFunction: ValidationFunction): Throwable? { + try { + validationFunction.function(instance) + } catch (e: Throwable) { + return e + } + return null +} + +/** + * loader class type should be ClassLoader in jvm + */ +internal actual fun ExecutionScenario.convertForLoader(loader: Any): ExecutionScenario { + return this +} + +actual fun chooseSequentialSpecification(sequentialSpecificationByUser: SequentialSpecification<*>?, testClass: TestClass): SequentialSpecification<*> = + sequentialSpecificationByUser ?: SequentialSpecification(testClass.create) + +actual fun storeCancellableContinuation(cont: CancellableContinuation<*>) { + TODO("Not yet implemented") +} + +internal actual fun executeActor( + instance: Any, + actor: Actor, + completion: Continuation? +): Result { + return try { + val r = actor.function(instance, actor.arguments) + ValueResult(r) + } catch (e: Throwable) { + if (actor.handledExceptions.any { it.safeCast(e) != null }) { // Do a cast. If == null, cast failed and e is not an instance of it + ExceptionResult(e::class, false) + } else { + throw e + } + } +} + +/** + * Collects the current thread dump and keeps only those + * threads that are related to the specified [runner]. + */ +internal actual fun collectThreadDump(runner: Runner): ThreadDump { + return ThreadDump() // No thread dumps in kotlin native +} + +internal actual fun StringBuilder.appendFailure(failure: LincheckFailure): StringBuilder { + when (failure) { + is IncorrectResultsFailure -> appendIncorrectResultsFailure(failure) + is DeadlockWithDumpFailure -> appendDeadlockWithDumpFailure(failure) + is UnexpectedExceptionFailure -> appendUnexpectedExceptionFailure(failure) + is ValidationFailure -> appendValidationFailure(failure) + is ObstructionFreedomViolationFailure -> appendObstructionFreedomViolationFailure(failure) + } + val results = if (failure is IncorrectResultsFailure) failure.results else null + if (failure.trace != null) { + appendLine() + appendLine("= The following interleaving leads to the error =") + appendLine("Sorry, there are no trace in kotlin native :(") + if (failure is DeadlockWithDumpFailure) { + appendLine() + append("All threads are in deadlock") + } + } + return this +} + +internal actual fun StringBuilder.appendDeadlockWithDumpFailure(failure: DeadlockWithDumpFailure): StringBuilder { + appendLine("= The execution has hung, see the thread dump =") + appendExecutionScenario(failure.scenario) + appendLine() + appendLine("Sorry, there are no threadDumps in kotlin native :(") + return this +} + +val STDERR = platform.posix.fdopen(2, "w") +actual fun printErr(message: String) { + fprintf(STDERR, message + "\n") + fflush(STDERR) +} + +internal actual fun nativeFreeze(any: Any): Unit { + any.freeze() +} +inline fun LincheckAtomicArray.toArray(): Array = Array(this.array.size) { this.array[it].value!! } +fun Array.toLincheckAtomicArray(): LincheckAtomicArray { + val ans = LincheckAtomicArray(this.size) + for (i in this.indices) { + ans.array[i].value = this[i] + } + return ans +} + +internal actual fun StringBuilder.appendStateEquivalenceViolationMessage(sequentialSpecification: SequentialSpecification<*>) { + append("To make verification faster, you can specify the state equivalence relation on your sequential specification.\n" + + "At the current moment, `${sequentialSpecification.function}` does not specify it, or the equivalence relation implementation is incorrect.\n" + + "To fix this, please implement `equals()` and `hashCode()` functions on `${sequentialSpecification.function}`; the simplest way is to extend `VerifierState`\n" + + "and override the `extractState()` function, which is called at once and the result of which is used for further `equals()` and `hashCode()` invocations.") +} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LincheckAssertionError.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/ScenarioBuilder.kt similarity index 67% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/LincheckAssertionError.kt rename to src/native/main/org/jetbrains/kotlinx/lincheck/ScenarioBuilder.kt index 08af2b5be..c1940f08f 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/LincheckAssertionError.kt +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/ScenarioBuilder.kt @@ -1,29 +1,29 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 - 2020 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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 -import org.jetbrains.kotlinx.lincheck.strategy.* -import java.lang.AssertionError +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario -class LincheckAssertionError( - failure: LincheckFailure -) : AssertionError("\n" + failure) \ No newline at end of file +actual class ScenarioBuilder actual constructor() { + actual fun buildScenario(): ExecutionScenario { + TODO("Should not be called") + } +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/ValidationFunction.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/ValidationFunction.kt new file mode 100644 index 000000000..d71ae275b --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/ValidationFunction.kt @@ -0,0 +1,48 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +import org.jetbrains.kotlinx.lincheck.execution.* + +actual class ValidationFunction( + val function: (Any) -> Unit, + val functionName: String +) + +actual val ValidationFunction.name: String get() = this.functionName + +actual class StateRepresentationFunction( + val function: (Any) -> String +) + +/** + * Contains information about the provided operations (see [Operation]). + * Several [tests][StressCTest] can refer to one structure + * (i.e. one test class could have several [StressCTest] annotations) + */ +actual class CTestStructure constructor( + actual val actorGenerators: List, + actual val operationGroups: List, + actual val validationFunctions: List, + actual val stateRepresentation: StateRepresentationFunction? +) { + +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/ValueResult.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/ValueResult.kt new file mode 100644 index 000000000..b79f06707 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/ValueResult.kt @@ -0,0 +1,38 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck + +/** + * Type of result used if the actor invocation returns any value. + */ +actual class ValueResult(actual val value: Any?, override val wasSuspended: Boolean = false) : Result(), Finalizable { + actual override fun equals(other: Any?): Boolean = if (other !is ValueResult) false else other.wasSuspended == wasSuspended && other.value == value + + actual override fun hashCode(): Int = if (wasSuspended) 0 else 1 // We can't use value here + + override fun toString() = wasSuspendedPrefix + "$value" + + override fun finalize() { + if(value is Finalizable) { + value.finalize() + } + } +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt new file mode 100644 index 000000000..998f84852 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/execution/ActorGenerator.kt @@ -0,0 +1,47 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.execution + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import kotlin.reflect.KClass + +actual class ActorGenerator( + val function: (Any, List) -> Any?, // (instance, arguments) -> result + val parameterGenerators: List>, + val functionName: String = function.toString(), + actual val useOnce: Boolean = false, + actual val isSuspendable: Boolean = false, + actual val handledExceptions: List> +) { + actual override fun toString(): String = functionName + + actual fun generate(threadId: Int): Actor { + val arguments = parameterGenerators.map { it.generate() }.map { if (it === THREAD_ID_TOKEN) threadId else it } + return Actor( + function = function, + arguments = arguments, + functionName = functionName, + isSuspendable = isSuspendable, + handledExceptions = handledExceptions + ) + } +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt new file mode 100644 index 000000000..883e807d9 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/FixedActiveThreadsExecutor.kt @@ -0,0 +1,159 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.runner + +import kotlin.native.concurrent.* +import kotlinx.cinterop.* +import platform.posix.* +import kotlinx.coroutines.* +import org.jetbrains.kotlinx.lincheck.* +import kotlin.math.* +import kotlin.native.ThreadLocal +import kotlin.native.internal.test.* +import kotlin.system.* + +internal class TestThread constructor(val iThread: Int, runnerHash: Int) { + val worker = AtomicReference(Worker.start(true, "Worker $iThread $runnerHash")) + val runnableFuture = AtomicReference?>(null) + + fun executeTask(r: () -> Any) { + runnableFuture.value = worker.value.execute(TransferMode.UNSAFE, { r }, { it.invoke() }) + } + + fun awaitLastTask(deadline: Long): Any { + while(deadline > currentTimeMillis()) { + if (runnableFuture.value!!.state.value == FutureState.COMPUTED.value) { + return runnableFuture.value!!.result + } + } + return FixedActiveThreadsExecutor.TIMEOUT + } + + fun terminate() { + // Don't want to terminate threads because of GC. Don't want to wait for result because of hanging + + //printErr("stop() $iThread called") + //val res = runnableFuture?.result + //println("terminate $iThread start") + //val result = runnableFuture!!.result + //worker.execute(TransferMode.UNSAFE, { }, { sleep(1000000000) }) + //println("terminate $iThread end") + //worker.value.requestTermination(false).result + //return res + //printErr("stop() $iThread finished") + } +} + +internal fun currentTimeMillis() = getTimeMillis() + +/** + * This executor maintains the specified number of threads and is used by + * [ParallelThreadsRunner] to execute [ExecutionScenario]-s. The main feature + * is that this executor keeps the re-using threads "hot" (active) as long as + * possible, so that they should not be parked and unparked between invocations. + */ +internal class FixedActiveThreadsExecutor(private val nThreads: Int, + runnerHash: Int, + private val initThreadFunction: (() -> Unit)? = null, + private val finishThreadFunction: (() -> Unit)? = null) { + // Threads used in this runner. + private val threads: LincheckAtomicArray = LincheckAtomicArray(nThreads) + private var operationsFinished: AtomicInt = AtomicInt(0) + + init { + (0 until nThreads).forEach { iThread -> + threads.array[iThread].value = TestThread(iThread, runnerHash).also { + it.executeTask { initThreadFunction?.invoke(); Any() } + val res = it.awaitLastTask(currentTimeMillis() + 10000) + if(res == FixedActiveThreadsExecutor.TIMEOUT) { + throw RuntimeException("initThreadFunction has hang") + } + } + } + } + + /** + * Submits the specified set of [tasks] to this executor + * and waits until all of them are completed. + * The number of tasks should be equal to [nThreads]. + * + * @throws LincheckTimeoutException if more than [timeoutMs] is passed. + * @throws LincheckExecutionException if an unexpected exception is thrown during the execution. + */ + fun submitAndAwait(tasks: Array, timeoutMs: Long) { + require(tasks.size == nThreads) + operationsFinished.value = 0 + submitTasks(tasks) + await(timeoutMs) + operationsFinished.value = 1 + } + + private fun submitTasks(tasks: Array) { + val testThreads = Array(nThreads){null} + for (i in 0 until nThreads) { + testThreads[i] = threads.array[i].value!! // Do atomic loads + } + testThreads.forEachIndexed{i, t -> t!!.executeTask { testThreadRunnable(i, tasks[i] as Runnable) }} // submit tasks + } + + private fun await(timeoutMs: Long) { + val deadline = currentTimeMillis() + timeoutMs + for (iThread in 0 until nThreads) + awaitTask(iThread, deadline) + } + + private fun awaitTask(iThread: Int, deadline: Long) { + val result = threads.array[iThread].value!!.awaitLastTask(deadline) + if (result == TIMEOUT) throw LincheckTimeoutException() + // Check whether there was an exception during the execution. + if (result != DONE) throw LincheckExecutionException(result as Throwable) + } + + private fun testThreadRunnable(iThread: Int, task: Runnable): Any { + val runnable = task + try { + runnable.run() + } catch (e: Throwable) { + return wrapInvalidAccessFromUnnamedModuleExceptionWithDescription(e) + } + return DONE + } + + fun close() { + // submit the shutdown task. + for (t in threads.toArray()) { + t.executeTask { finishThreadFunction?.invoke(); Any() } + if(operationsFinished.value == 1) { + // Operations executed before deadline, it is safe to wait for finish + val res = t.awaitLastTask(currentTimeMillis() + 10000) + if(res == FixedActiveThreadsExecutor.TIMEOUT) { + throw RuntimeException("finishThreadFunction has hang") + } + } + t.terminate() + } + } + + companion object { + internal val TIMEOUT = Any().freeze() + private val DONE = Any() + } +} diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt new file mode 100644 index 000000000..4db83f8bf --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/ParallelThreadsRunner.kt @@ -0,0 +1,159 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.runner + +import kotlinx.cinterop.* +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* +import kotlin.random.* + +/** + * This runner executes parallel scenario part in different threads. + * Supports running scenarios with `suspend` functions. + * + * It is pretty useful for stress testing or if you do not care about context switch expenses. + */ +internal actual open class ParallelThreadsRunner actual constructor( + strategy: Strategy, + testClass: TestClass, + validationFunctions: List, + stateRepresentationFunction: StateRepresentationFunction?, + private val timeoutMs: Long, + private val useClocks: UseClocks, + initThreadFunction: (() -> Unit)?, + finishThreadFunction: (() -> Unit)?) : Runner(strategy, testClass, validationFunctions, stateRepresentationFunction) { + private val runnerHash = this.hashCode() // helps to distinguish this runner threads from others + + private val executor = FixedActiveThreadsExecutor(scenario.threads, runnerHash, initThreadFunction, finishThreadFunction).freeze() // should be closed in `close()` + + private val testThreadExecutions = AtomicReference>(LincheckAtomicArray(0)) + + init { + this.ensureNeverFrozen() + } + + override fun initialize() { + super.initialize() + val arr = Array(scenario.threads) { t -> + TestThreadExecution(t, scenario.parallelExecution[t]).freeze() + } + testThreadExecutions.value = arr.toLincheckAtomicArray() + testThreadExecutions.value.toArray().forEach { it.allThreadExecutions.value = testThreadExecutions.value } + } + + private fun reset() { + testThreadExecutions.value.toArray().forEachIndexed { t, ex -> + val threads = scenario.threads + val actors = scenario.parallelExecution[t].size + ex.useClocks.value = if (useClocks == UseClocks.ALWAYS) 1 else (if (Random.nextBoolean()) 1 else 0) + ex.curClock.value = 0 + ex.clocks.value = Array(actors) { emptyClockArray(threads) }.toLincheckAtomicArray() + ex.results.value = LincheckAtomicArray(actors) + } + completedOrSuspendedThreads.set(0) + } + + override fun constructStateRepresentation(): String? { + throw RuntimeException("should not be called") + } + + fun nativeConstructStateRepresentation(testInstance: Any) = + stateRepresentationFunction?.function?.invoke(testInstance) as String? + + override fun run(): InvocationResult { + reset() + val testInstance = testClass.createInstance() + testInstance.ensureNeverFrozen() + val initResults = scenario.initExecution.mapIndexed { i, initActor -> + executeActor(testInstance, initActor).also { + executeValidationFunctions(testInstance, validationFunctions) { functionName, exception -> + val s = ExecutionScenario( + scenario.initExecution.subList(0, i + 1), + emptyList(), + emptyList() + ) + return ValidationFailureInvocationResult(s, functionName, exception) + } + } + } + val afterInitStateRepresentation = nativeConstructStateRepresentation(testInstance) + try { + executor.submitAndAwait(testThreadExecutions.value.toArray().map { NativeTestThreadExecution(testInstance, it) }.toTypedArray(), timeoutMs) + } catch (e: LincheckTimeoutException) { + val threadDump = collectThreadDump(this) + return DeadlockInvocationResult(threadDump) + } catch (e: LincheckExecutionException) { + return UnexpectedExceptionInvocationResult(e.cause!!) + } + val parallelResultsWithClock = testThreadExecutions.value.toArray().map { ex -> + ex.results.value.toArray().zip(ex.clocks.value.toArray()).map { ResultWithClock(it.first!!, HBClock(it.second)) } + } + executeValidationFunctions(testInstance, validationFunctions) { functionName, exception -> + val s = ExecutionScenario( + scenario.initExecution, + scenario.parallelExecution, + emptyList() + ) + return ValidationFailureInvocationResult(s, functionName, exception) + } + val afterParallelStateRepresentation = nativeConstructStateRepresentation(testInstance) + val dummyCompletion = Continuation(EmptyCoroutineContext) {} + var postPartSuspended = false + val postResults = scenario.postExecution.mapIndexed { i, postActor -> + // no actors are executed after suspension of a post part + val result = if (postPartSuspended) { + NoResult + } else { + // post part may contain suspendable actors if there aren't any in the parallel part, invoke with dummy continuation + executeActor(testInstance, postActor, dummyCompletion).also { + postPartSuspended = it.wasSuspended + } + } + executeValidationFunctions(testInstance, validationFunctions) { functionName, exception -> + val s = ExecutionScenario( + scenario.initExecution, + scenario.parallelExecution, + scenario.postExecution.subList(0, i + 1) + ) + return ValidationFailureInvocationResult(s, functionName, exception) + } + result + } + val afterPostStateRepresentation = nativeConstructStateRepresentation(testInstance) + if (testInstance is Finalizable) { + testInstance.finalize() + } + val results = ExecutionResult( + initResults, afterInitStateRepresentation, + parallelResultsWithClock, afterParallelStateRepresentation, + postResults, afterPostStateRepresentation + ) + return CompletedInvocationResult(results) + } + + override fun close() { + super.close() + executor.close() + } +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.kt new file mode 100644 index 000000000..69b50b157 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/TestThreadExecution.kt @@ -0,0 +1,83 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.runner + +import kotlinx.coroutines.* +import org.jetbrains.kotlinx.lincheck.* +import kotlin.native.concurrent.* +import kotlin.native.concurrent.AtomicInt +import kotlin.reflect.* + +class NativeTestThreadExecution(val testInstance: Any, val threadExecution: TestThreadExecution) : Runnable { + override fun run() { + threadExecution.run(testInstance) + } +} + +class TestThreadExecution(val iThread: Int, val actors: List) { + val allThreadExecutions = AtomicReference>(LincheckAtomicArray(0)) + + val results = AtomicReference>(LincheckAtomicArray(0)) // for ExecutionResult + val clocks = AtomicReference>(LincheckAtomicArray(0)) // for HBClock + + var curClock = AtomicInt(0) + var useClocks = AtomicInt(0) // 0 -- false, 1 -- true + + fun readClocks(currentActor: Int) { + val arr = allThreadExecutions.value.toArray() + for (i in arr.indices) { + clocks.value.array[currentActor].value!!.array[i].value = arr[i].curClock.value + } + } + + fun incClock() { + curClock.increment() + } + + fun run(testInstance: Any) { + //printErr("RUN $iThread #1") + //runner.onStart(iThread) + actors.forEachIndexed { index, actor -> + //printErr("RUN $iThread #2 $index") + readClocks(index) + //runner.onActorStart(iThread) + //Load arguments for operation + val result: Result = try { + val r = actor.function(testInstance, actor.arguments) + //printErr("ValueResult") + ValueResult(r) + } catch (e: Throwable) { + if (actor.handledExceptions.any { it.safeCast(e) != null }) { // Do a cast. If == null, cast failed and e is not an instance of it + //printErr("ExceptionResult") + ExceptionResult(e::class, false) + } else { + //printErr("FailureResult with $e") + //runner.onFailure(iThread, e) + throw e + } + } + results.value.array[index].value = result + incClock() + } + //printErr("RUN $iThread #finish ") + //runner.onFinish(iThread) + } +} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/DummySequentialSpecification.java b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/ThreadDump.kt similarity index 67% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/DummySequentialSpecification.java rename to src/native/main/org/jetbrains/kotlinx/lincheck/runner/ThreadDump.kt index a14037775..0321c0f94 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/verifier/DummySequentialSpecification.java +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/ThreadDump.kt @@ -1,26 +1,23 @@ -/*- - * #%L +/* * Lincheck - * %% - * Copyright (C) 2019 JetBrains s.r.o. - * %% + * + * Copyright (C) 2019 - 2021 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.verifier; -public class DummySequentialSpecification { - private DummySequentialSpecification() {} // This dummy class should not be created -} +package org.jetbrains.kotlinx.lincheck.runner + +actual class ThreadDump \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/runner/loadClassLoader.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/loadClassLoader.kt new file mode 100644 index 000000000..6602ef317 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/runner/loadClassLoader.kt @@ -0,0 +1,46 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.runner + +import org.jetbrains.kotlinx.lincheck.* +import kotlin.native.concurrent.* + +// TODO check + +actual fun Runner.loadClassLoader(): Any { + return Any() +} + +actual fun loadClass(classLoader: Any, testClass: TestClass): TestClass { + return testClass +} + +actual class AtomicInteger actual constructor(value: Int) { + val atomicValue = AtomicInt(value) + + actual fun get(): Int { + return atomicValue.value + } + + actual fun set(value: Int): Unit { + atomicValue.value = value + } +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt new file mode 100644 index 000000000..d63c6caf7 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt @@ -0,0 +1,41 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.strategy + +import org.jetbrains.kotlinx.lincheck.execution.* + +/** + * Implementation of this class describes how to run the generated execution. + * + * Note that strategy can run execution several times. For strategy creating + * [.createStrategy] method is used. It is impossible to add a new strategy + * without any code change. + */ +actual abstract class Strategy protected actual constructor(actual val scenario: ExecutionScenario) { + + actual abstract fun run(): LincheckFailure? + + /** + * Is invoked before each actor execution. + */ + actual open fun onActorStart(iThread: Int) { + } +} \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/Trace.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/Trace.kt new file mode 100644 index 000000000..c0857b7f0 --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/Trace.kt @@ -0,0 +1,23 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +package org.jetbrains.kotlinx.lincheck.strategy + +actual class Trace \ No newline at end of file diff --git a/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt b/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt new file mode 100644 index 000000000..6f4afe51f --- /dev/null +++ b/src/native/main/org/jetbrains/kotlinx/lincheck/strategy/stress/StressStrategy.kt @@ -0,0 +1,94 @@ +/*- + * #%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.strategy.stress + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.runner.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.verifier.* + +actual class StressStrategy actual constructor( + private val testCfg: StressCTestConfiguration, + private val testClass: TestClass, + scenario: ExecutionScenario, + private val validationFunctions: List, + private val stateRepresentationFunction: StateRepresentationFunction?, + private val verifier: Verifier +) : Strategy(scenario) { + private var invocations = testCfg.invocationsPerIteration + private var initThreadFunction = testCfg.initThreadFunction + private var finishThreadFunction = testCfg.finishThreadFunction + private var runner: Runner + + init { + runner = ParallelThreadsRunner( + strategy = this, + testClass = testClass, + validationFunctions = validationFunctions, + stateRepresentationFunction = stateRepresentationFunction, + timeoutMs = testCfg.timeoutMs, + useClocks = UseClocks.RANDOM, + initThreadFunction = initThreadFunction, + finishThreadFunction = finishThreadFunction + ) + try { + runner.initialize() + } catch (t: Throwable) { + runner.close() + throw t + } + } + + actual override fun run(): LincheckFailure? { + // Run invocations + initThreadFunction?.invoke() + try { + for (invocation in 0 until invocations) { + runner.also { + //println("invocation $invocation has started") + //val t1 = currentTimeMillis() + val ir = runner.run() + //val t2 = currentTimeMillis() + //println("invocation $invocation has invocated, took ${t2 - t1}ms to run") + when (ir) { + is CompletedInvocationResult -> { + //printErr("Try Verify") + if (!verifier.verifyResults(scenario, ir.results)) { + return IncorrectResultsFailure(scenario, ir.results) + } else { + ir.finalize() + } + } + else -> return ir.toLincheckFailure(scenario) + } + //val t3 = currentTimeMillis() + //println("invocation $invocation has verified, took ${t3 - t2}ms to verify") + } + } + } finally { + finishThreadFunction?.invoke() + runner.close() + } + return null + } +} \ No newline at end of file diff --git a/src/native/test/AbstractLincheckTest.kt b/src/native/test/AbstractLincheckTest.kt new file mode 100644 index 000000000..34b8a6694 --- /dev/null +++ b/src/native/test/AbstractLincheckTest.kt @@ -0,0 +1,79 @@ +/*- + * #%L + * Lincheck + * %% + * Copyright (C) 2019 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% + */ + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.native.* +import kotlin.reflect.* +import kotlin.test.* + +abstract class AbstractLincheckStressTest( + private vararg val expectedFailures: KClass +) : VerifierState() { + open fun > T.customize() {} + override fun extractState(): Any = this.identityHashCode() + + private fun , I> T.runInternalTest() { + val failure: LincheckFailure? = checkImpl() + if (failure === null) { + assert(expectedFailures.isEmpty()) { + "This test should fail, but no error has been occurred (see the logs for details)" + } + } else { + failure.trace?.let { checkTraceHasNoLincheckEvents(it.toString()) } + assert(expectedFailures.contains(failure::class)) { + "This test has failed with an unexpected error: \n $failure" + } + } + } + + @Test + fun testWithStressStrategy(): Unit = LincheckStressConfiguration().run { + commonConfiguration() + runInternalTest() + } +/* + @Test + fun testWithModelCheckingStrategy(): Unit = ModelCheckingOptions().run { + commonConfiguration() + runInternalTest() + } +*/ + private fun > T.commonConfiguration(): Unit = run { + iterations(250) + invocationsPerIteration(100) + actorsBefore(3) + threads(3) + actorsPerThread(4) + actorsAfter(3) + minimizeFailedScenario(false) + customize() + } +} + + +fun checkTraceHasNoLincheckEvents(trace: String) { + val testPackageOccurrences = trace.split("org.jetbrains.kotlinx.lincheck.test.").size - 1 + val lincheckPackageOccurrences = trace.split("org.jetbrains.kotlinx.lincheck.").size - 1 + check(testPackageOccurrences == lincheckPackageOccurrences) { "Internal Lincheck events were found in the trace" } +} diff --git a/src/native/test/ExceptionAsResultTest.kt b/src/native/test/ExceptionAsResultTest.kt new file mode 100644 index 000000000..f39950c73 --- /dev/null +++ b/src/native/test/ExceptionAsResultTest.kt @@ -0,0 +1,59 @@ +/* +* #%L +* Lincheck +* %% +* Copyright (C) 2015 - 2018 Devexperts, LLC +* %% +* 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% +*/ + +import org.jetbrains.kotlinx.lincheck.* +import kotlin.test.* + +class ExceptionAsResultTest { + fun npeIsOk() { + (null as String?)!![0] + } + + fun subclassExceptionIsOk() { + if ((0..1).random() == 1) throw IndexOutOfBoundsException(MESSAGE) else throw IllegalStateException(MESSAGE) + } + + @Test + fun test() { + try { + LincheckStressConfiguration("ExceptionAsResultTest").apply { + iterations(1) + invocationsPerIteration(500) + requireStateEquivalenceImplCheck(false) + + initialState { ExceptionAsResultTest() } + + operation(ExceptionAsResultTest::npeIsOk, "npeIsOk", listOf(NullPointerException::class)) + operation(ExceptionAsResultTest::subclassExceptionIsOk, "subclassExceptionIsOk", listOf(Throwable::class)) + }.runTest() + fail("Should fail with AssertionError") + } catch (e: AssertionError) { + val m = e.message + assertTrue(m!!.contains("IllegalStateException") || m.contains("IndexOutOfBoundsException")) + assertFalse(m.contains(MESSAGE)) + } + } + + companion object { + private const val MESSAGE = "iujdhfgilurtybfu" + } +} \ No newline at end of file diff --git a/src/native/test/FirstTest.kt b/src/native/test/FirstTest.kt new file mode 100644 index 000000000..c09173d04 --- /dev/null +++ b/src/native/test/FirstTest.kt @@ -0,0 +1,150 @@ +/* + * Lincheck + * + * Copyright (C) 2019 - 2021 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 + * + */ + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.strategy.IncorrectResultsFailure +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.native.concurrent.* +import kotlin.test.* + +class ComplexMutableClass(val i: Int, val j: String) { + + override fun equals(other: Any?): Boolean { + return if (other is ComplexMutableClass) { + i == other.i && j == other.j; + } else { + false; + } + } + + override fun toString(): String { + return "ComplexMutableClass $i $j" + } +} + +class TestClass : VerifierState() { + val atomicState: AtomicInt = AtomicInt(0) + var regularState: Int = 0 + + override fun extractState(): Any { + return Pair(atomicState.value, regularState) + } + + override fun toString(): String { + return "$atomicState $regularState" + } + + fun increment(): Int { + regularState++ + return regularState + } + + fun decrement(): Int { + regularState-- + return regularState + } + + fun complexOperation(): ComplexMutableClass { + return ComplexMutableClass(239, "Hello, world!") + } + + fun atomicIncrement() = atomicState.addAndGet(1) + + fun atomicDecrement() = atomicState.addAndGet(-1) +} + +class FirstTest { + @Test + fun test_failing() { + val f = LincheckStressConfiguration("FirstTest_1").apply { + iterations(300) + invocationsPerIteration(500) + actorsBefore(2) + threads(3) + actorsPerThread(4) + actorsAfter(2) + minimizeFailedScenario(false) + + initialState { TestClass() } + stateRepresentation { this.toString() } + + operation(TestClass::increment, "add") + operation({ this.decrement() }, "decrement") + operation(IntGen(""), BooleanGen(""), { _, _ -> + //println("Operation with arguments $i and $b has called") + }, "do_nothing") + }.checkImpl() + + assert(f != null && f is IncorrectResultsFailure) { + "This test should fail with a incorrect results error" + } + } + + @Test + fun test_working() { + LincheckStressConfiguration("FirstTest_2").apply { + iterations(10) + invocationsPerIteration(500) + actorsBefore(2) + threads(3) + actorsPerThread(5) + actorsAfter(2) + minimizeFailedScenario(false) + + initialState { TestClass() } + stateRepresentation { this.toString() } + + operation(TestClass::atomicIncrement, "atomicIncrement") + operation(TestClass::atomicDecrement, "atomicDecrement") + }.runTest() + } + + @Test + fun test_many_threads() { + LincheckStressConfiguration("FirstTest_3").apply { + iterations(100) + invocationsPerIteration(20) + threads(7) + actorsPerThread(2) + minimizeFailedScenario(false) + + initialState { TestClass() } + + operation(TestClass::atomicIncrement, "atomicIncrement") + operation(TestClass::atomicDecrement, "atomicDecrement") + operation(TestClass::complexOperation, "complexOperation") + }.runTest() + } + + @Test + fun test_complex() { + LincheckStressConfiguration("FirstTest_3").apply { + iterations(10) + invocationsPerIteration(500) + threads(4) + minimizeFailedScenario(false) + + initialState { TestClass() } + + operation(TestClass::complexOperation, "complexOperation") + }.runTest() + } +} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ShortGen.java b/src/native/test/HangingTest.kt similarity index 52% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ShortGen.java rename to src/native/test/HangingTest.kt index bad387bfe..c4c414852 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ShortGen.java +++ b/src/native/test/HangingTest.kt @@ -1,10 +1,8 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - -/* +/*- * #%L * Lincheck * %% - * Copyright (C) 2015 - 2018 Devexperts, LLC + * Copyright (C) 2019 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 @@ -22,15 +20,25 @@ * #L% */ -public class ShortGen implements ParameterGenerator { - private final IntGen intGen; +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.strategy.* - public ShortGen(String configuration) { - intGen = new IntGen(configuration); - intGen.checkRange(Short.MIN_VALUE, Short.MAX_VALUE, "short"); +class HangingTest : AbstractLincheckStressTest(DeadlockWithDumpFailure::class) { + fun badOperation() { + while (true) {} } - public Short generate() { - return (short) (int) intGen.generate(); + override fun > T.customize() { + iterations(1) + invocationsPerIteration(500) + actorsBefore(0) + actorsAfter(0) + requireStateEquivalenceImplCheck(false) + minimizeFailedScenario(false) + invocationTimeout(100) + + initialState { HangingTest() } + + operation(HangingTest::badOperation) } } diff --git a/src/native/test/NonParallelOpGroupTest.kt b/src/native/test/NonParallelOpGroupTest.kt new file mode 100644 index 000000000..1d191cc69 --- /dev/null +++ b/src/native/test/NonParallelOpGroupTest.kt @@ -0,0 +1,99 @@ +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.test.* + +/* + * Lincheck + * %% + * Copyright (C) 2015 - 2018 Devexperts, LLC + * %% + * 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 + * . + */ + +class NonParallelOpGroupTest : VerifierState() { + private var producerCounter = 0 + private var consumerCounter = 0 + + fun produce(count: Int): Int { + producerCounter += count + return producerCounter + } + + fun produce2(count: Int): Int { + producerCounter -= count + return producerCounter + } + + fun consume(): Int { + consumerCounter++ + return consumerCounter + } + + fun consume2(): Int { + consumerCounter-- + return consumerCounter + } + + override fun extractState(): Any { + return Pair(producerCounter, consumerCounter) + } + + @Test + fun test_failing() { + val f = LincheckStressConfiguration("NonParallelOpGroupTest").apply { + iterations(500) + invocationsPerIteration(100) + actorsBefore(2) + threads(2) + actorsPerThread(5) + actorsAfter(2) + minimizeFailedScenario(false) + + initialState { NonParallelOpGroupTest() } + stateRepresentation { this.toString() } + + operation(IntGen(""), NonParallelOpGroupTest::produce, "produce") + operation(IntGen(""), NonParallelOpGroupTest::produce, "produce2") + operation(NonParallelOpGroupTest::consume, "consume") + operation(NonParallelOpGroupTest::consume, "consume2") + }.checkImpl() + assert(f != null && f is IncorrectResultsFailure) { + "This test should fail with a incorrect results error" + } + } + + @Test + fun test_working() { + LincheckStressConfiguration("NonParallelOpGroupTest").apply { + iterations(10) + invocationsPerIteration(500) + actorsBefore(2) + threads(2) + actorsPerThread(5) + actorsAfter(2) + minimizeFailedScenario(false) + + initialState { NonParallelOpGroupTest() } + stateRepresentation { this.toString() } + + operation(IntGen(""), NonParallelOpGroupTest::produce, "produce", nonParallelGroupName = "produce") + operation(IntGen(""), NonParallelOpGroupTest::produce2, "produce2", nonParallelGroupName = "produce") + operation(NonParallelOpGroupTest::consume, "consume", nonParallelGroupName = "consume") + operation(NonParallelOpGroupTest::consume2, "consume2", nonParallelGroupName = "consume") + }.runTest() + } +} \ No newline at end of file diff --git a/src/native/test/RunOnceTest.kt b/src/native/test/RunOnceTest.kt new file mode 100644 index 000000000..2f8277727 --- /dev/null +++ b/src/native/test/RunOnceTest.kt @@ -0,0 +1,66 @@ +/* +* #%L +* Lincheck +* %% +* Copyright (C) 2015 - 2018 Devexperts, LLC +* %% +* 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% +*/ + +import org.jetbrains.kotlinx.lincheck.LincheckStressConfiguration +import kotlinx.atomicfu.locks.* +import kotlin.test.Test + +class RunOnceTest { + private val state: A = A() + + fun a() { + state.a() + } + + fun b() { + state.b() + } + + @Test + fun test() { + LincheckStressConfiguration("RunOnceTest").apply { + threads(3) + iterations(10) + invocationsPerIteration(10) + requireStateEquivalenceImplCheck(false) + + initialState { RunOnceTest() } + operation(RunOnceTest::a, useOnce = true) + operation(RunOnceTest::b, useOnce = true) + }.runTest() + } + + internal inner class A : SynchronizedObject() { + private var a = false + private var b = false + + fun a() = synchronized(this) { + if (a) throw AssertionError() + a = true + } + + fun b() = synchronized(this) { + if (b) throw AssertionError() + b = true + } + } +} \ No newline at end of file diff --git a/src/native/test/ScenarioGenerationParameterDiversityTest.kt b/src/native/test/ScenarioGenerationParameterDiversityTest.kt new file mode 100644 index 000000000..3f2d4795b --- /dev/null +++ b/src/native/test/ScenarioGenerationParameterDiversityTest.kt @@ -0,0 +1,60 @@ +/*- + * #%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% + */ + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.test.* + +/** + * This test checks that parameters in generated scenarios are diversified + */ +class ScenarioGenerationParameterDiversityTest : VerifierState() { + fun foo(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) { + check(setOf(a, b, c, d, e, f).size > 1) { "At least 2 parameters should be different w.h.p." } + } + + fun bar(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) { + check(setOf(a, b, c, d, e, f).size > 1) { "At least 2 parameters should be different w.h.p." } + } + + @Test + fun test() { + // TODO maybe create API like DefaultGens.getDefaultIntGen or something + val defaultGen = IntGen("") + val paramGen = IntGen("") + LincheckStressConfiguration("ScenarioGenerationParameterDiversityTest").apply { + invocationsPerIteration(1) + iterations(100) + threads(1) + + initialState { ScenarioGenerationParameterDiversityTest() } + + operation(listOf(defaultGen, defaultGen, defaultGen, defaultGen, defaultGen, defaultGen), + { arg -> foo(arg[0] as Int, arg[1] as Int, arg[2] as Int, arg[3] as Int, arg[4] as Int, arg[5] as Int) }) + operation(listOf(paramGen, paramGen, paramGen, paramGen, paramGen, paramGen), + { arg -> bar(arg[0] as Int, arg[1] as Int, arg[2] as Int, arg[3] as Int, arg[4] as Int, arg[5] as Int) }) + }.runTest() + } + + override fun extractState(): Any = 0 // constant state +} diff --git a/src/native/test/StateEquivalenceImplCheckTest.kt b/src/native/test/StateEquivalenceImplCheckTest.kt new file mode 100644 index 000000000..94a45ba73 --- /dev/null +++ b/src/native/test/StateEquivalenceImplCheckTest.kt @@ -0,0 +1,45 @@ +/*- + * #%L + * Lincheck + * %% + * Copyright (C) 2019 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% + */ + +import org.jetbrains.kotlinx.lincheck.* +import kotlin.native.concurrent.* +import kotlin.test.* + +class StateEquivalenceImplCheckTest { + private var i = AtomicInt(0) + + fun incAndGet() = i.addAndGet(1) + + @Test + fun test() { + try { + LincheckStressConfiguration("StateEquivalenceImplCheckTest").apply { + initialState { StateEquivalenceImplCheckTest() } + + operation(StateEquivalenceImplCheckTest::incAndGet) + }.runTest() + } catch(e: IllegalStateException) { + // Check that IllegalStateException is thrown if `requireStateEquivalenceImplCheck` option is true by default + // and hashCode/equals methods are not implemented + } + } +} diff --git a/src/native/test/ThreadIdTest.kt b/src/native/test/ThreadIdTest.kt new file mode 100644 index 000000000..6c0f82c02 --- /dev/null +++ b/src/native/test/ThreadIdTest.kt @@ -0,0 +1,50 @@ +/*- + * #%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% + */ + +import kotlinx.atomicfu.* +import org.jetbrains.kotlinx.lincheck.LincheckStressConfiguration +import org.jetbrains.kotlinx.lincheck.paramgen.* + +class ThreadIdTest : AbstractLincheckStressTest() { + private val balances = IntArray(5) + private val counter = atomic(0) + + fun inc(threadId: Any): Int = counter.incrementAndGet() + .also { /*println("inc $threadId");*/ balances[threadId as Int]++ } + + fun decIfNotNegative(threadId: Any) { + //println("decIfNotNegative $threadId"); + if (balances[threadId as Int] == 0) return + balances[threadId]-- + val c = counter.decrementAndGet() + if (c < 0) error("The counter cannot be negative") + } + + override fun extractState() = balances.toList() to counter.value + + override fun > T.customize() { + initialState { ThreadIdTest() } + + operation(ThreadIdGen(""), ThreadIdTest::inc) + operation(ThreadIdGen(""), ThreadIdTest::decIfNotNegative) + } +} diff --git a/src/native/test/UnexpectedExceptionTest.kt b/src/native/test/UnexpectedExceptionTest.kt new file mode 100644 index 000000000..677918163 --- /dev/null +++ b/src/native/test/UnexpectedExceptionTest.kt @@ -0,0 +1,51 @@ +/*- + * #%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% + */ + +import org.jetbrains.kotlinx.lincheck.LincheckStressConfiguration +import org.jetbrains.kotlinx.lincheck.strategy.* +import kotlin.native.concurrent.* + +class UnexpectedExceptionTest : AbstractLincheckStressTest(UnexpectedExceptionFailure::class) { + private var canEnterForbiddenSection: AtomicInt = AtomicInt(0) + + fun operation1() { + // atomic because of possible compile-time optimization + canEnterForbiddenSection.increment() + canEnterForbiddenSection.decrement() + } + + fun operation2() { + check(canEnterForbiddenSection.value == 0) + } + + override fun extractState(): Any = canEnterForbiddenSection.value + + override fun > T.customize() { + iterations(500) + invocationsPerIteration(200) + + initialState { UnexpectedExceptionTest() } + + operation(UnexpectedExceptionTest::operation1) + operation(UnexpectedExceptionTest::operation2, "operation2", listOf(IllegalArgumentException::class)) + } +} \ No newline at end of file diff --git a/src/native/test/ValidateFunctionTest.kt b/src/native/test/ValidateFunctionTest.kt new file mode 100644 index 000000000..d2493c96a --- /dev/null +++ b/src/native/test/ValidateFunctionTest.kt @@ -0,0 +1,69 @@ +/*- + * #%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% + */ + +import org.jetbrains.kotlinx.lincheck.LincheckStressConfiguration +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.native.concurrent.* +import kotlin.test.* + +class ValidateFunctionTest : VerifierState() { + val c = AtomicInt(0) + + fun inc() = c.addAndGet(1) + + override fun extractState() = c.value + + fun validateNoError() {} + + var validateInvoked: Int = 0 + + // This function fails on the 5ht invocation + fun validateWithError() { + validateInvoked++ + if (validateInvoked == 5) error("Validation works!") + } + + @Test + fun test() { + val f = LincheckStressConfiguration("ValidateFunctionTest").apply { + iterations(1) + invocationsPerIteration(1) + actorsBefore(3) + actorsAfter(10) + + initialState { ValidateFunctionTest() } + operation(ValidateFunctionTest::inc) + validationFunction(ValidateFunctionTest::validateNoError, "validateNoError") + validationFunction(ValidateFunctionTest::validateWithError, "validateWithError") + }.checkImpl() + assert(f != null && f is ValidationFailure && f.functionName == "validateWithError") { + "This test should fail with a validation error" + } + val validationInvocations = f!!.scenario.initExecution.size + f.scenario.postExecution.size + 1 + assert(validationInvocations == 5) { + "The scenario should have exactly 5 points to invoke validation functions, " + + "see the resulting scenario below: \n${f.scenario}" + } + } + +} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ByteGen.java b/src/native/test/verifier/EpsilonVerifierTest.kt similarity index 52% rename from src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ByteGen.java rename to src/native/test/verifier/EpsilonVerifierTest.kt index 9b887c8d1..cfbbde0d0 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/paramgen/ByteGen.java +++ b/src/native/test/verifier/EpsilonVerifierTest.kt @@ -1,5 +1,3 @@ -package org.jetbrains.kotlinx.lincheck.paramgen; - /* * #%L * Lincheck @@ -10,27 +8,42 @@ * 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 verifier -public class ByteGen implements ParameterGenerator { - private final IntGen intGen; +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.test.* - public ByteGen(String configuration) { - intGen = new IntGen(configuration); - intGen.checkRange(Byte.MIN_VALUE, Byte.MAX_VALUE, "byte"); - } +class EpsilonVerifierTest : VerifierState() { + private var i = 0 + + fun incAndGet() = i++ // non-atomic! - public Byte generate() { - return (byte) (int) intGen.generate(); + @Test + fun test() { + LincheckStressConfiguration("EpsilonVerifierTest").apply { + iterations(5) + invocationsPerIteration(500) + threads(2) + actorsPerThread(2) + verifier { s -> EpsilonVerifier(s) } + + initialState { EpsilonVerifierTest() } + + operation(EpsilonVerifierTest::incAndGet) + }.runTest() } + + override fun extractState() = i } diff --git a/src/native/test/verifier/SequentialSpecificationTest.kt b/src/native/test/verifier/SequentialSpecificationTest.kt new file mode 100644 index 000000000..59483eb23 --- /dev/null +++ b/src/native/test/verifier/SequentialSpecificationTest.kt @@ -0,0 +1,67 @@ +/*- + * #%L + * Lincheck + * %% + * Copyright (C) 2019 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 verifier + +import AbstractLincheckStressTest +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import kotlin.native.concurrent.* + +interface CounterInterface { + fun set(value: Int) + fun get(): Int +} + +class TestCounter : VerifierState(), CounterInterface { + private val c = AtomicInt(0) + + override fun set(value: Int) { + c.value = value + } + + override fun get() = c.value + 1 + override fun extractState() = c.value +} + +class SequentialSpecificationTest : AbstractLincheckStressTest(IncorrectResultsFailure::class) { + override fun > T.customize() { + sequentialSpecification(SequentialSpecification { CorrectCounter() }) + + initialState { TestCounter() } + + operation(CounterInterface::get) + operation(IntGen(""), CounterInterface::set) + } +} + + +class CorrectCounter : VerifierState(), CounterInterface { + private var c = 0 + override fun set(value: Int) { + c = value + } + + override fun get(): Int = c + override fun extractState() = c +} diff --git a/src/native/test/verifier/linearizability/ClocksTest.kt b/src/native/test/verifier/linearizability/ClocksTest.kt new file mode 100644 index 000000000..77c885742 --- /dev/null +++ b/src/native/test/verifier/linearizability/ClocksTest.kt @@ -0,0 +1,103 @@ +/*- + * #%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 verifier.linearizability + +import AbstractLincheckStressTest +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.strategy.* + +interface Clocks { + fun a() + fun b() + fun c() + fun d(): Int +} + +class ClocksTest : AbstractLincheckStressTest(IncorrectResultsFailure::class), Clocks { + private var bStarted = false + + override fun a() { + // do nothing + } + + override fun b() { + bStarted = true + } + + override fun c() { + while (!bStarted) { + } // wait until `a()` is completed + } + + override fun d(): Int { + return 0 // cannot return 0, should fail + } + + override fun > T.customize() { + executionGenerator { c: CTestConfiguration, s: CTestStructure -> ClocksTestScenarioGenerator(c, s) } + iterations(1) + invocationsPerIteration(500) + sequentialSpecification(SequentialSpecification { ClocksTestSequential() }) + requireStateEquivalenceImplCheck(false) + minimizeFailedScenario(false) + + initialState { ClocksTest() } + + operation(Clocks::a) + operation(Clocks::b) + operation(Clocks::c) + operation(Clocks::d) + } +} + +class ClocksTestScenarioGenerator(testCfg: CTestConfiguration, testStructure: CTestStructure) + : ExecutionGenerator(testCfg, testStructure) { + override fun nextExecution() = ExecutionScenario( + emptyList(), + listOf( + listOf( + Actor(function = {instance, _ -> (instance as Clocks).a()}, arguments = emptyList()), + Actor(function = {instance, _ -> (instance as Clocks).b()}, arguments = emptyList()) + ), + listOf( + Actor(function = {instance, _ -> (instance as Clocks).c()}, arguments = emptyList()), + Actor(function = {instance, _ -> (instance as Clocks).d()}, arguments = emptyList()) + ) + ), + emptyList() + ) + +} + +class ClocksTestSequential : Clocks { + private var x = 0 + + override fun a() { + x = 1 + } + + override fun b() {} + override fun c() {} + + override fun d(): Int = x +} diff --git a/src/native/test/verifier/linearizability/CounterTests.kt b/src/native/test/verifier/linearizability/CounterTests.kt new file mode 100644 index 000000000..18b41d38c --- /dev/null +++ b/src/native/test/verifier/linearizability/CounterTests.kt @@ -0,0 +1,114 @@ +/*- + * #%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 verifier.linearizability + +import AbstractLincheckStressTest +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.strategy.* +import kotlin.native.concurrent.* +import kotlin.reflect.KClass + +interface CounterTest { + fun incAndGet(): Int +} + +abstract class AbstractCounterTest( + private val counter: Counter, + vararg expectedErrors: KClass +) : AbstractLincheckStressTest(*expectedErrors), CounterTest { + override fun incAndGet(): Int = counter.incAndGet() + + fun > T.customizeOperations() { + operation(CounterTest::incAndGet) + } + + override fun extractState(): Any = counter.get() +} + +class CounterCorrectTest : AbstractCounterTest(CounterCorrect()) { + override fun > T.customize() { + customizeOperations() + + initialState { CounterCorrectTest() } + } +} + +class CounterWrong0Test : AbstractCounterTest(CounterWrong0(), IncorrectResultsFailure::class) { + override fun > T.customize() { + customizeOperations() + + initialState { CounterWrong0Test() } + } +} + +class CounterWrong1Test : AbstractCounterTest(CounterWrong1(), IncorrectResultsFailure::class) { + override fun > T.customize() { + customizeOperations() + + initialState { CounterWrong1Test() } + } +} + +class CounterWrong2Test : AbstractCounterTest(CounterWrong2(), IncorrectResultsFailure::class) { + override fun > T.customize() { + customizeOperations() + + initialState { CounterWrong2Test() } + } +} + +interface Counter { + fun incAndGet(): Int + fun get(): Int +} + +private class CounterWrong0 : Counter { + private var c: Int = 0 + + override fun incAndGet(): Int = ++c + override fun get(): Int = c +} + +private class CounterWrong1 : Counter { + private var c: Int = 0 + + override fun incAndGet(): Int { + c++ + return c + } + + override fun get(): Int = c +} + +private class CounterWrong2 : Counter { + private var c: Int = 0 + + override fun incAndGet(): Int = ++c + override fun get(): Int = c +} + +private class CounterCorrect : Counter { + private val c = AtomicInt(0) + + override fun incAndGet(): Int = c.addAndGet(1) + override fun get(): Int = c.value +} diff --git a/src/native/test/verifier/linearizability/HashMapTest.kt b/src/native/test/verifier/linearizability/HashMapTest.kt new file mode 100644 index 000000000..9dbc861aa --- /dev/null +++ b/src/native/test/verifier/linearizability/HashMapTest.kt @@ -0,0 +1,47 @@ +/*- + * #%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 verifier.linearizability + +import AbstractLincheckStressTest +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.strategy.* + +class HashMapTest : AbstractLincheckStressTest(IncorrectResultsFailure::class, UnexpectedExceptionFailure::class) { + private val m = HashMap() + + fun put(key: Int, value: Int): Int? = m.put(key, value) + + operator fun get(key: Int?): Int? = m[key] + + override fun > T.customize() { + initialState { HashMapTest() } + + val keyGen = IntGen("") + + operation(IntGen(""), keyGen, HashMapTest::put) + operation(keyGen, HashMapTest::get) + } + + override fun extractState(): Any = m +} + diff --git a/src/native/test/verifier/linearizability/LockBasedSetTests.kt b/src/native/test/verifier/linearizability/LockBasedSetTests.kt new file mode 100644 index 000000000..e84e9a4e2 --- /dev/null +++ b/src/native/test/verifier/linearizability/LockBasedSetTests.kt @@ -0,0 +1,134 @@ +/*- + * #%L + * Lincheck + * %% + * Copyright (C) 2019 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 verifier.linearizability + +import AbstractLincheckStressTest +import kotlinx.atomicfu.locks.* +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.paramgen.* +import kotlin.native.concurrent.* + +abstract class AbstractSetTest(private val set: Set) : AbstractLincheckStressTest() { + fun add(key: Int): Boolean = set.add(key) + + fun remove(key: Int): Boolean = set.remove(key) + + operator fun contains(key: Int): Boolean = set.contains(key) + + fun > T.customizeOperations() { + val keyGen = IntGen("1:5") + operation(keyGen, AbstractSetTest::add) + operation(keyGen, AbstractSetTest::remove) + operation(keyGen, AbstractSetTest::contains) + } + + override fun extractState(): Any = (1..5).map { set.contains(it) } +} + +class SpinLockSetTest : AbstractSetTest(SpinLockBasedSet()) { + override fun > T.customize() { + initialState { SpinLockSetTest() } + + customizeOperations() + } +} + +class ReentrantLockSetTest : AbstractSetTest(ReentrantLockBasedSet()) { + override fun > T.customize() { + initialState { ReentrantLockSetTest() } + + customizeOperations() + } +} + +class SynchronizedLockSetTest : AbstractSetTest(SynchronizedBlockBasedSet()) { + override fun > T.customize() { + initialState { SynchronizedLockSetTest() } + + customizeOperations() + } +} + +interface Set { + fun add(key: Int): Boolean + fun remove(key: Int): Boolean + fun contains(key: Int): Boolean +} + +internal class SpinLockBasedSet : Set { + private val set = mutableSetOf() + private val locked = AtomicInt(0) + + override fun add(key: Int): Boolean = withSpinLock { + set.add(key) + } + + override fun remove(key: Int): Boolean = withSpinLock { + set.remove(key) + } + + override fun contains(key: Int): Boolean = withSpinLock { + set.contains(key) + } + + private fun withSpinLock(block: () -> Boolean): Boolean { + while (!locked.compareAndSet(0, 1)) { /* Thread.yield() in java */ } + try { + return block() + } finally { + locked.value = 0 + } + } +} + +internal class ReentrantLockBasedSet : Set { + private val set = mutableSetOf() + private val lock = ReentrantLock() + + override fun add(key: Int): Boolean = lock.withLock { + set.add(key) + } + + override fun remove(key: Int): Boolean = lock.withLock { + set.remove(key) + } + + override fun contains(key: Int): Boolean = lock.withLock { + set.contains(key) + } +} + +internal class SynchronizedBlockBasedSet : SynchronizedObject(), Set { + private val set = mutableSetOf() + + override fun add(key: Int): Boolean = synchronized(this) { + set.add(key) + } + + override fun remove(key: Int): Boolean = synchronized(this) { + set.remove(key) + } + + override fun contains(key: Int): Boolean = synchronized(this) { + set.contains(key) + } +} \ No newline at end of file