├── project ├── build.properties ├── metals.sbt ├── plugins.sbt └── Publish.scala ├── .scalafmt.conf ├── .gitignore ├── macros └── src │ └── main │ └── scala │ └── lotos │ └── macros │ ├── package.scala │ ├── MacroUtils.scala │ ├── SymbolMacros.scala │ ├── ShapelessMacros.scala │ └── TestConstructor.scala ├── model └── src │ └── main │ ├── scala │ └── lotos │ │ └── model │ │ ├── Consistency.scala │ │ ├── TestResult.scala │ │ ├── Scenario.scala │ │ ├── TestConfig.scala │ │ ├── SpecT.scala │ │ ├── Gen.scala │ │ ├── TestLog.scala │ │ └── PrintLogs.scala │ ├── scala-2.13 │ └── lotos │ │ └── model │ │ └── MethodT.scala │ └── scala-2.12 │ └── lotos │ └── model │ └── MethodT.scala ├── testing └── src │ ├── test │ └── scala │ │ └── lotos │ │ └── testing │ │ └── LtsSuite.scala │ └── main │ ├── scala-2.12 │ └── lotos │ │ └── testing │ │ └── syntax.scala │ ├── scala-2.13 │ └── lotos │ │ └── testing │ │ └── syntax.scala │ └── scala │ └── lotos │ └── testing │ └── LotosTest.scala ├── internal └── src │ └── main │ └── scala │ └── lotos │ └── internal │ ├── testing │ ├── Invoke.scala │ ├── TestRun.scala │ └── lts.scala │ └── deepcopy │ ├── package.scala │ ├── FastBytesInputStream.scala │ └── FastBytesOutputStream.scala ├── examples └── src │ └── main │ └── scala │ └── lotos │ └── examples │ ├── TrieMapTest.scala │ ├── HashMapTest.scala │ └── LFStackTest.scala ├── LICENSE ├── publish.sbt └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.7 -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.3.2" 2 | maxColumn = 120 3 | align = most -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .metals/ 3 | .bloop/ 4 | .project/metals.sbt 5 | target/ 6 | */target/ 7 | *.class 8 | *.log 9 | -------------------------------------------------------------------------------- /macros/src/main/scala/lotos/macros/package.scala: -------------------------------------------------------------------------------- 1 | package lotos 2 | 3 | package object macros { 4 | type NList[T] = List[(String, T)] 5 | type MethodDecl[T] = (List[NList[T]], T) 6 | } 7 | -------------------------------------------------------------------------------- /project/metals.sbt: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT! This file is auto-generated. 2 | // This file enables sbt-bloop to create bloop config files. 3 | 4 | addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-105-118a551b") 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") 2 | 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1") 4 | 5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8.1") 6 | -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/Consistency.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | sealed trait Consistency 4 | 5 | object Consistency { 6 | case object sequential extends Consistency 7 | case object linearizable extends Consistency 8 | } 9 | -------------------------------------------------------------------------------- /testing/src/test/scala/lotos/testing/LtsSuite.scala: -------------------------------------------------------------------------------- 1 | //package lotos.internal.testing 2 | //import org.scalatest.flatspec.AsyncFlatSpec 3 | //import scala.collection.mutable.Stack 4 | // 5 | //class LtsSuite extends AsyncFlatSpec { 6 | // behavior.of("lts") 7 | // 8 | // val stackSpec = 9 | // 10 | // 11 | // 12 | //} -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/TestResult.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | sealed trait TestResult 4 | 5 | case object TestSuccess extends TestResult 6 | case class TestFailure(history: Vector[Vector[TestLog]]) extends TestResult 7 | case class TestTimeout(history: Vector[Vector[TestLog]]) extends TestResult 8 | -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/Scenario.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | import scala.util.Random 4 | 5 | case class Scenario(actions: Vector[Vector[String]]) 6 | 7 | object Scenario { 8 | def gen(methods: Vector[String], parallelism: Int, length: Int): Scenario = 9 | Scenario(Vector.fill(parallelism)(Random.shuffle(methods.flatMap(Vector.fill(length)(_))).take(length))) 10 | } 11 | -------------------------------------------------------------------------------- /internal/src/main/scala/lotos/internal/testing/Invoke.scala: -------------------------------------------------------------------------------- 1 | package lotos.internal.testing 2 | 3 | import lotos.model.TestLog 4 | 5 | import scala.concurrent.duration.FiniteDuration 6 | 7 | trait Invoke[F[_]] extends Serializable { 8 | def copy: Invoke[F] 9 | def invoke(method: String, timeout: FiniteDuration): F[TestLog] 10 | def invokeWithSeeds(method: String, seeds: Map[String, Long]): F[TestLog] 11 | def methods: Vector[String] 12 | } 13 | -------------------------------------------------------------------------------- /project/Publish.scala: -------------------------------------------------------------------------------- 1 | import sbt.SettingKey 2 | object Publish { 3 | val publishVersion: SettingKey[String] = SettingKey( 4 | label = "publishVersion", 5 | description = "version prefix, it will be *the* version of module if branch is master" 6 | ) 7 | 8 | val publishName: SettingKey[String] = SettingKey( 9 | label = "publishName", 10 | description = "module name, it will be prefixed with lotos- in the artifact name" 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /testing/src/main/scala-2.12/lotos/testing/syntax.scala: -------------------------------------------------------------------------------- 1 | package lotos.testing 2 | 3 | import lotos.model.{MethodT, SpecT} 4 | import shapeless.{HNil, Witness} 5 | 6 | package object syntax { 7 | def method[Name <: String](name: Witness.Aux[Name]): MethodT[Name, HNil] = 8 | new MethodT(name = name.value, paramGens = Map.empty) 9 | 10 | def spec[I](construct: => I): SpecT[I, HNil] = 11 | new SpecT(construct = () => construct, methods = Map.empty) 12 | } 13 | -------------------------------------------------------------------------------- /testing/src/main/scala-2.13/lotos/testing/syntax.scala: -------------------------------------------------------------------------------- 1 | package lotos.testing 2 | 3 | import lotos.model.{MethodT, SpecT} 4 | import shapeless.{HNil, Witness} 5 | 6 | package object syntax { 7 | def method[Name <: String with Singleton](name: Name): MethodT[Name, HNil] = 8 | new MethodT(name = name, paramGens = Map.empty) 9 | 10 | def spec[I](construct: => I): SpecT[I, HNil] = 11 | new SpecT(construct = () => construct, methods = Map.empty) 12 | } 13 | -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/TestConfig.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | import scala.concurrent.duration._ 4 | 5 | sealed trait FailureFormat 6 | 7 | object FailureFormat { 8 | case object Console extends FailureFormat 9 | case class Diagram(fileName: String) extends FailureFormat 10 | case class Both(fileName: String) extends FailureFormat 11 | } 12 | 13 | case class TestConfig( 14 | parallelism: Int = 2, 15 | scenarioLength: Int = 10, 16 | scenarioRepetition: Int = 100, 17 | scenarioCount: Int = 100, 18 | operationTimeout: FiniteDuration = 1.second, 19 | failureFormat: FailureFormat = FailureFormat.Console 20 | ) 21 | -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/SpecT.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | import shapeless.{::, HList} 4 | 5 | class SpecT[I, Methods <: HList]( 6 | protected val construct: () => I, 7 | protected val methods: Map[String, AnyRef], 8 | ) { 9 | def withMethod[Name, ParamGens <: HList](m: MethodT[Name, ParamGens]): SpecT[I, MethodT[Name, ParamGens] :: Methods] = 10 | new SpecT(construct = this.construct, methods = methods + (MethodT.name(m) -> m)) 11 | } 12 | 13 | object SpecT { 14 | def construct[I, Methods <: HList](specT: SpecT[I, Methods]): I = specT.construct() 15 | 16 | def methods[I, Methods <: HList](specT: SpecT[I, Methods]): Map[String, AnyRef] = specT.methods 17 | } 18 | -------------------------------------------------------------------------------- /model/src/main/scala-2.13/lotos/model/MethodT.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | import shapeless.labelled.FieldType 4 | import shapeless.{::, HList} 5 | 6 | class MethodT[Name, Params <: HList]( 7 | protected val name: String, 8 | protected val paramGens: Map[String, AnyRef] 9 | ) { 10 | def param[Key <: String with Singleton, T](key: Key)( 11 | implicit gen: Gen[T]): MethodT[Name, FieldType[Key, T] :: Params] = 12 | new MethodT(name, paramGens + (key -> gen)) 13 | } 14 | 15 | object MethodT { 16 | def name[Name, Params <: HList](methodT: MethodT[Name, Params]): String = 17 | methodT.name 18 | 19 | def paramGens[Name, Params <: HList](methodT: MethodT[Name, Params]): Map[String, AnyRef] = 20 | methodT.paramGens 21 | } 22 | -------------------------------------------------------------------------------- /model/src/main/scala-2.12/lotos/model/MethodT.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | import shapeless.labelled.FieldType 4 | import shapeless.{::, HList, Witness} 5 | 6 | class MethodT[Name, Params <: HList]( 7 | protected val name: String, 8 | protected val paramGens: Map[String, AnyRef] 9 | ) { 10 | def param[Key <: String, T](key: Witness.Aux[Key])( 11 | implicit gen: Gen[T]): MethodT[Name, FieldType[Key, T] :: Params] = 12 | new MethodT(name, paramGens + (key.value -> gen)) 13 | } 14 | 15 | object MethodT { 16 | def name[Name, Params <: HList](methodT: MethodT[Name, Params]): String = 17 | methodT.name 18 | 19 | def paramGens[Name, Params <: HList](methodT: MethodT[Name, Params]): Map[String, AnyRef] = 20 | methodT.paramGens 21 | } 22 | -------------------------------------------------------------------------------- /internal/src/main/scala/lotos/internal/deepcopy/package.scala: -------------------------------------------------------------------------------- 1 | package lotos.internal 2 | 3 | import java.io.{ObjectInputStream, ObjectOutputStream} 4 | 5 | import cats.effect.Sync 6 | import cats.implicits._ 7 | import scala.util.Try 8 | 9 | package object deepcopy { 10 | def deepCopy[T <: AnyRef](orig: T): Either[Throwable, T] = { 11 | Try { 12 | val fbos = new FastBytesOutputStream(); 13 | val out = new ObjectOutputStream(fbos) 14 | out.writeObject(orig) 15 | out.flush() 16 | out.close() 17 | val in = new ObjectInputStream(fbos.getInputStream) 18 | in.readObject.asInstanceOf[T] 19 | }.toEither 20 | } 21 | 22 | def deepCopyF[F[_]: Sync, T <: AnyRef](orig: T): F[T] = Sync[F].delay(deepCopy(orig)).rethrow 23 | } 24 | -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/Gen.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | trait Gen[T] { 4 | def gen(seed: Long): T 5 | def show(t: T): String 6 | } 7 | 8 | object Gen extends GenPrimitiveInstances 9 | 10 | trait GenPrimitiveInstances { 11 | def int(limit: Int = 10): Gen[Int] = new Gen[Int] { 12 | def gen(seed: Long): Int = (seed % limit).toInt 13 | 14 | def show(t: Int): String = t.toString 15 | } 16 | 17 | def string(length: Int = 5): Gen[String] = new Gen[String] { 18 | val alphabet = ('a' to 'z').zipWithIndex.map { case (a, b) => (b.toLong, a) }.toMap 19 | def gen(seed: Long): String = 20 | new String(Array.fill(length)("").zipWithIndex.map { 21 | case (_, ind) => 22 | alphabet(Math.abs((seed << ind) % 26)) 23 | }) 24 | 25 | def show(t: String): String = t 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/src/main/scala/lotos/examples/TrieMapTest.scala: -------------------------------------------------------------------------------- 1 | package lotos.examples 2 | 3 | import cats.effect.{ExitCode, IO, IOApp} 4 | import lotos.model.{Consistency, Gen, TestConfig} 5 | import lotos.testing.LotosTest 6 | import lotos.testing.syntax.{method, spec} 7 | 8 | import scala.collection.concurrent.TrieMap 9 | 10 | /*_*/ 11 | object TrieMapTest extends IOApp { 12 | val trieMapSpec = 13 | spec(new TrieMap[Int, String]) 14 | .withMethod(method("put").param("key")(Gen.int(5)).param("value")(Gen.string(1))) 15 | .withMethod(method("get").param("k")(Gen.int(5))) 16 | 17 | val cfg = TestConfig(parallelism = 3, scenarioLength = 6, scenarioRepetition = 100, scenarioCount = 10) 18 | 19 | override def run(args: List[String]): IO[ExitCode] = 20 | for { 21 | _ <- LotosTest.forSpec(trieMapSpec, cfg, Consistency.linearizable) 22 | } yield ExitCode.Success 23 | 24 | } 25 | -------------------------------------------------------------------------------- /internal/src/main/scala/lotos/internal/deepcopy/FastBytesInputStream.scala: -------------------------------------------------------------------------------- 1 | package lotos.internal.deepcopy 2 | 3 | import java.io.InputStream 4 | 5 | class FastBytesInputStream(buf: Array[Byte], count: Int) extends InputStream { 6 | 7 | var pos: Int = 0 8 | 9 | override final def available: Int = count - pos 10 | 11 | override final def read: Int = 12 | if (pos < count) buf({ 13 | pos += 1; pos - 1 14 | }) & 0xff 15 | else -1 16 | 17 | override final def read(b: Array[Byte], off: Int, length: Int): Int = { 18 | val newLength = 19 | if (pos >= count) -1 20 | else if (pos + length > count) count - pos 21 | else length 22 | System.arraycopy(buf, pos, b, off, newLength) 23 | pos += newLength 24 | newLength 25 | } 26 | 27 | override final def skip(amount: Long): Long = { 28 | val n: Long = 29 | if (pos + amount > count) count - pos 30 | else if (amount < 0) 0 31 | else amount 32 | pos += n.toInt 33 | n 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vasiliy Morkovkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /macros/src/main/scala/lotos/macros/MacroUtils.scala: -------------------------------------------------------------------------------- 1 | package lotos.macros 2 | import scala.reflect.NameTransformer 3 | import scala.reflect.macros.{TypecheckException, blackbox} 4 | 5 | trait MacroUtils { 6 | val c: blackbox.Context 7 | 8 | import c.universe._ 9 | 10 | def abort(s: String) = c.abort(c.enclosingPosition, s) 11 | def info(s: String) = c.info(c.enclosingPosition, s, force = true) 12 | 13 | def typeCheckOrAbort(t: Tree): Tree = 14 | try c.typecheck(t) 15 | catch { 16 | case ex: TypecheckException => c.abort(c.enclosingPosition, ex.toString) 17 | } 18 | 19 | def extractMethods(tpe: Type): NList[MethodDecl[Type]] = 20 | tpe.decls.collect { 21 | case s: MethodSymbol => 22 | symbolName(s) -> 23 | (s.infoIn(tpe) 24 | .paramLists 25 | .map(lst => lst.map(p => symbolName(p) -> p.typeSignature)) -> 26 | s.infoIn(tpe).resultType) 27 | }.toList 28 | 29 | def unpackString(sType: Type): String = 30 | sType match { 31 | case ConstantType(Constant(s: String)) => NameTransformer.encode(s) 32 | case x => abort(s"$x should be a string constant") 33 | } 34 | 35 | def symbolName(symbol: Symbol): String = symbol.name.decodedName.toString 36 | } 37 | -------------------------------------------------------------------------------- /internal/src/main/scala/lotos/internal/deepcopy/FastBytesOutputStream.scala: -------------------------------------------------------------------------------- 1 | package lotos.internal.deepcopy 2 | 3 | import java.io.OutputStream 4 | 5 | class FastBytesOutputStream(val initSize: Int = 5 * 1024) extends OutputStream { 6 | var buf: Array[Byte] = new Array[Byte](initSize) 7 | var size = 0 8 | 9 | private def verifyBufferSize(sz: Int): Unit = { 10 | if (sz > buf.length) { 11 | var old = buf 12 | buf = new Array[Byte](Math.max(sz, 2 * buf.length)) 13 | System.arraycopy(old, 0, buf, 0, old.length) 14 | old = null 15 | } 16 | } 17 | 18 | def getSize: Int = size 19 | 20 | def getByteArray: Array[Byte] = buf 21 | 22 | override final def write(b: Array[Byte]): Unit = { 23 | verifyBufferSize(size + b.length) 24 | System.arraycopy(b, 0, buf, size, b.length) 25 | size += b.length 26 | } 27 | 28 | override final def write(b: Array[Byte], off: Int, len: Int): Unit = { 29 | verifyBufferSize(size + len) 30 | System.arraycopy(b, off, buf, size, len) 31 | size += len 32 | } 33 | 34 | final def write(b: Int): Unit = { 35 | verifyBufferSize(size + 1) 36 | buf({ 37 | size += 1; size - 1 38 | }) = b.toByte 39 | } 40 | 41 | def reset(): Unit = { 42 | size = 0 43 | } 44 | 45 | def getInputStream = new FastBytesInputStream(buf, size) 46 | } 47 | -------------------------------------------------------------------------------- /macros/src/main/scala/lotos/macros/SymbolMacros.scala: -------------------------------------------------------------------------------- 1 | package lotos.macros 2 | 3 | import shapeless.ReprTypes 4 | 5 | import scala.reflect.macros.blackbox 6 | 7 | trait SymbolMacros extends ReprTypes { 8 | val c: blackbox.Context 9 | 10 | import c.internal.{constantType, refinedType} 11 | import c.universe._ 12 | 13 | def taggedType = typeOf[shapeless.tag.Tagged[_]].typeConstructor 14 | 15 | object KeyName { 16 | def apply(name: String): Type = 17 | NamedSymbol(appliedType(taggedType, constantType(Constant(name)))) 18 | 19 | def unapply(tpe: Type): Option[String] = tpe match { 20 | case NamedSymbol(ConstantType(Constant(name: String))) => Some(name) 21 | case ConstantType(Constant(name: String)) => Some(name) 22 | case _ => None 23 | } 24 | } 25 | 26 | object NamedSymbol { 27 | def apply(tpe: Type): Type = refinedType(List(symbolTpe, tpe), NoSymbol) 28 | 29 | def unapply(tpe: Type): Option[Type] = tpe.dealias match { 30 | case RefinedType(List(sym, tag, _*), _) if sym == symbolTpe => tag.typeArgs.headOption 31 | case _ => None 32 | } 33 | } 34 | 35 | def freshIdent(name: String): Ident = Ident(freshName(name)) 36 | def freshName(name: String): TermName = TermName(c.freshName(name)) 37 | 38 | } 39 | -------------------------------------------------------------------------------- /publish.sbt: -------------------------------------------------------------------------------- 1 | import Publish._ 2 | 3 | publishVersion := "0.1.0" 4 | 5 | ThisBuild / organization := "dev.susliko" 6 | ThisBuild / version := { 7 | val branch = git.gitCurrentBranch.value 8 | if (branch == "master") publishVersion.value 9 | else s"${publishVersion.value}-$branch-SNAPSHOT" 10 | } 11 | 12 | ThisBuild / publishMavenStyle := true 13 | 14 | ThisBuild / publishTo := 15 | (if (!isSnapshot.value) { 16 | sonatypePublishToBundle.value 17 | } else { 18 | Some(Opts.resolver.sonatypeSnapshots) 19 | }) 20 | 21 | ThisBuild / scmInfo := Some( 22 | ScmInfo( 23 | url("https://github.com/susliko/lotos"), 24 | "git@github.com:susliko/lotos" 25 | ) 26 | ) 27 | 28 | ThisBuild / developers := List( 29 | Developer( 30 | id = "susliko", 31 | name = "Vasiliy Morkovkin", 32 | email = "1istoobig@gmail.com", 33 | url = url("https://github.com/susliko") 34 | ) 35 | ) 36 | 37 | ThisBuild / description := "Library for testing concurrent data structures" 38 | ThisBuild / licenses := List("MIT" -> new URL("https://opensource.org/licenses/MIT")) 39 | ThisBuild / homepage := Some(url("https://github.com/susliko/lotos")) 40 | 41 | // Remove all additional repository other than Maven Central from POM 42 | ThisBuild / pomIncludeRepository := { _ => 43 | false 44 | } 45 | 46 | ThisBuild / credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credential") 47 | -------------------------------------------------------------------------------- /examples/src/main/scala/lotos/examples/HashMapTest.scala: -------------------------------------------------------------------------------- 1 | package lotos.examples 2 | 3 | import cats.effect.{ExitCode, IO, IOApp} 4 | import lotos.model.{Consistency, FailureFormat, Gen, TestConfig} 5 | import lotos.testing.LotosTest 6 | import lotos.testing.syntax.{method, spec} 7 | import java.{util => ju} 8 | 9 | import scala.concurrent.duration._ 10 | 11 | class UnsafeHashMap extends Serializable { 12 | val underlying = new ju.HashMap[Int, String] 13 | def put(key: Int, value: String): Option[String] = Option(underlying.put(key, value)) 14 | def get(key: Int): String = { 15 | Thread.sleep(1500) 16 | Option(underlying.get(key)).getOrElse(throw new NoSuchElementException) 17 | } 18 | } 19 | 20 | /*_*/ 21 | object HashMapTest extends IOApp { 22 | val hashMapSpec = 23 | spec(new UnsafeHashMap) 24 | .withMethod(method("put").param("key")(Gen.int(1)).param("value")(Gen.string(1))) 25 | .withMethod(method("get").param("key")(Gen.int(1))) 26 | 27 | val cfg = TestConfig( 28 | parallelism = 2, 29 | scenarioLength = 2, 30 | scenarioRepetition = 20, 31 | scenarioCount = 5, 32 | operationTimeout = 1.second, 33 | failureFormat = FailureFormat.Diagram("test.html") 34 | ) 35 | 36 | override def run(args: List[String]): IO[ExitCode] = 37 | for { 38 | _ <- LotosTest.forSpec(hashMapSpec, cfg, Consistency.linearizable) 39 | } yield ExitCode.Success 40 | } 41 | -------------------------------------------------------------------------------- /internal/src/main/scala/lotos/internal/testing/TestRun.scala: -------------------------------------------------------------------------------- 1 | package lotos.internal.testing 2 | 3 | import cats.effect.{Concurrent, ContextShift, Timer} 4 | import cats.Parallel 5 | import cats.effect.concurrent.Ref 6 | import cats.implicits._ 7 | import lotos.internal.deepcopy._ 8 | import lotos.model.MethodResp.Timeout 9 | import lotos.model.{Scenario, TestLog} 10 | 11 | import scala.concurrent.duration.FiniteDuration 12 | 13 | trait TestRun[F[_]] { 14 | def run(scenario: Scenario, operationTimeout: FiniteDuration): F[Vector[Vector[TestLog]]] 15 | } 16 | 17 | case class TestRunImpl[F[_]: Concurrent: Parallel: Timer](invoke: Invoke[F])(cs: ContextShift[F]) extends TestRun[F] { 18 | def run(scenario: Scenario, operationTimeout: FiniteDuration): F[Vector[Vector[TestLog]]] = 19 | for { 20 | inv <- deepCopyF(invoke) 21 | logs <- scenario.actions.parTraverse(act => 22 | for { 23 | logsRef <- Ref.of(Vector.empty[TestLog]) 24 | _ <- cs.shift 25 | _ <- act.collectFirstSomeM( 26 | method => 27 | for { 28 | log <- inv.invoke(method, operationTimeout) 29 | _ <- logsRef.update(_ :+ log) 30 | result = log.resp match { 31 | case Timeout(_) => Some(()) 32 | case _ => None 33 | } 34 | } yield result 35 | ) 36 | logs <- logsRef.get 37 | } yield logs) 38 | } yield logs 39 | } 40 | -------------------------------------------------------------------------------- /examples/src/main/scala/lotos/examples/LFStackTest.scala: -------------------------------------------------------------------------------- 1 | package lotos.examples 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | 5 | import cats.effect.{ExitCode, IO, IOApp} 6 | import lotos.model.{Consistency, Gen, TestConfig} 7 | import lotos.testing.LotosTest 8 | import lotos.testing.syntax._ 9 | 10 | import scala.concurrent.duration._ 11 | 12 | case class Node[T](value: T, next: Node[T] = null) 13 | 14 | class LFStack[T] extends Serializable { 15 | val stack: AtomicReference[Node[T]] = new AtomicReference[Node[T]]() 16 | 17 | def push(item: T): Unit = { 18 | var oldHead: Node[T] = null 19 | var newHead = Node(item) 20 | do { 21 | oldHead = stack.get(); 22 | newHead = newHead.copy(next = oldHead) 23 | } while (!stack.compareAndSet(oldHead, newHead)) 24 | } 25 | 26 | def pop: Option[T] = { 27 | var oldHead: Node[T] = null 28 | var newHead: Node[T] = null 29 | do { 30 | oldHead = stack.get() 31 | if (oldHead == null) 32 | return None 33 | 34 | newHead = oldHead.next 35 | } while (!stack.compareAndSet(oldHead, newHead)) 36 | Some(oldHead.value) 37 | } 38 | } 39 | 40 | object LFStackTest extends IOApp { 41 | val stackSpec = 42 | spec(new LFStack[Int]) 43 | .withMethod(method("push").param("item")(Gen.int(10))) 44 | .withMethod(method("pop")) 45 | 46 | val cfg = TestConfig( 47 | parallelism = 2, 48 | scenarioLength = 10, 49 | scenarioRepetition = 100, 50 | scenarioCount = 10 51 | ) 52 | 53 | def run(args: List[String]): IO[ExitCode] = 54 | for { 55 | _ <- LotosTest.forSpec(stackSpec, cfg, Consistency.linearizable) 56 | } yield ExitCode.Success 57 | } 58 | -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/TestLog.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | import cats.Eq 4 | import lotos.model.MethodResp.{Fail, Ok, Timeout} 5 | 6 | case class TestLog( 7 | call: MethodCall, 8 | resp: MethodResp 9 | ) { 10 | def show(callTime: Long, respTime: Long): String = { 11 | val respShow = resp match { 12 | case Ok(result, _) => result 13 | case Fail(error, _) => error.toString 14 | case Timeout(_) => "TIMEOUT" 15 | } 16 | s"[$callTime; $respTime] ${call.methodName}(${call.params}): $respShow" 17 | } 18 | } 19 | 20 | object TestLog { 21 | implicit val catsEq: Eq[TestLog] = Eq.instance((a, b) => { 22 | MethodResp.catsEq.eqv(a.resp, b.resp) && MethodCall.catsEq.eqv(a.call, b.call) 23 | }) 24 | } 25 | 26 | sealed trait MethodResp { 27 | def timestamp: Long 28 | } 29 | 30 | object MethodResp { 31 | case class Ok(result: String, timestamp: Long) extends MethodResp 32 | case class Fail(error: Throwable, timestamp: Long) extends MethodResp 33 | case class Timeout(timestamp: Long) extends MethodResp 34 | 35 | implicit val catsEq: Eq[MethodResp] = Eq.instance((a, b) => { 36 | (a, b) match { 37 | case (Ok(resA, _), Ok(resB, _)) if resA == resB => true 38 | case (Fail(errA, _), Fail(errB, _)) if errA.toString == errB.toString => true 39 | case _ => false 40 | } 41 | }) 42 | } 43 | 44 | case class MethodCall(methodName: String, paramSeeds: Map[String, Long], params: String, timestamp: Long) 45 | 46 | object MethodCall { 47 | implicit val catsEq: Eq[MethodCall] = Eq.instance { 48 | case (a, b) => 49 | a.methodName == b.methodName && a.paramSeeds == b.paramSeeds && a.params == b.params 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/maven-central/v/dev.susliko/lotos-testing_2.13.svg)](https://search.maven.org/search?q=dev.susliko.lotos-testing) 2 | 3 | A library for testing concurrent data structures that you lacked! 4 | 5 | *Lotos* aims to provide: 6 | 1. Simple DSL for specifications 7 | 2. Configurable generator of test scenarios 8 | 3. Various consistency model checkers 9 | 4. Verbose consistency violation reports 10 | 11 | # Quick example 12 | 13 | Having some implementation of a data structure: 14 | ```scala 15 | import java.{util => ju} 16 | 17 | class UnsafeHashMap extends Serializable { 18 | val underlying = new ju.HashMap[Int, String] 19 | def put(key: Int, value: String): Option[String] = Option(underlying.put(key, value)) 20 | def get(key: Int): Option[String] = Option(underlying.get(key)) 21 | } 22 | ``` 23 | 24 | describe its specification via Lotos DSL and run the test: 25 | ```scala 26 | object UnsafeHashMapTest extends IOApp { 27 | val hashMapSpec = 28 | spec(new UnsafeHashMap) 29 | .withMethod( 30 | method("put") 31 | .param("key")(Gen.intGen(1)) 32 | .param("value")(Gen.stringGen(1))) 33 | .withMethod( 34 | method("get") 35 | .param("key")(Gen.intGen(1))) 36 | 37 | val cfg = TestConfig(parallelism = 2, scenarioLength = 2, scenarioRepetition = 3, scenarioCount = 5) 38 | 39 | def run(args: List[String]): IO[ExitCode] = 40 | LotosTest.forSpec(hashMapSpec, cfg, Consistency.sequential) as ExitCode.Success 41 | } 42 | ``` 43 | 44 | In case of failure it will provide a scenario which failed to comply with specified guaranties (e.g. sequential consistency): 45 | 46 | ``` 47 | Testing scenario 1 48 | Test failed for scenario: 49 | put(key = 0, value = i): None | get(key = 0): None 50 | get(key = 0): Some(i) | put(key = 0, value = t): None 51 | ``` 52 | -------------------------------------------------------------------------------- /macros/src/main/scala/lotos/macros/ShapelessMacros.scala: -------------------------------------------------------------------------------- 1 | package lotos.macros 2 | 3 | import shapeless.ReprTypes 4 | 5 | import scala.annotation.tailrec 6 | import scala.reflect.macros.blackbox 7 | 8 | trait ShapelessMacros extends ReprTypes with MacroUtils with SymbolMacros { 9 | val c: blackbox.Context 10 | import c.universe._ 11 | 12 | def unfoldCompoundTpe(compoundTpe: Type, nil: Type, cons: Type): List[Type] = { 13 | @tailrec 14 | def loop(tpe: Type, acc: List[Type]): List[Type] = 15 | tpe.dealias match { 16 | case TypeRef(_, consSym, List(hd, tl)) if consSym.asType.toType.typeConstructor =:= cons => loop(tl, hd :: acc) 17 | case `nil` => acc 18 | case _ => abort(s"Bad compound type $compoundTpe") 19 | } 20 | loop(compoundTpe, Nil).reverse 21 | } 22 | 23 | def hlistElements(tpe: Type): List[Type] = 24 | unfoldCompoundTpe(tpe, hnilTpe, hconsTpe) 25 | 26 | def extractRecord(tpe: Type): List[(String, Type)] = 27 | hlistElements(tpe).flatMap { 28 | case FieldType(KeyName(name), value) => List(name -> value) 29 | case _ => Nil 30 | } 31 | 32 | object FieldType { 33 | import internal._ 34 | 35 | def apply(kTpe: Type, vTpe: Type): Type = 36 | refinedType(List(vTpe, typeRef(keyTagTpe, keyTagTpe.typeSymbol, List(kTpe, vTpe))), NoSymbol) 37 | 38 | def unapply(fTpe: Type): Option[(Type, Type)] = { 39 | val KeyTagPre = keyTagTpe 40 | fTpe.dealias match { 41 | case RefinedType(List(v0, TypeRef(_, sym, List(k, v1))), _) 42 | if sym.asType.toType.typeConstructor =:= KeyTagPre && v0 =:= v1 => 43 | Some((k, v0)) 44 | case _ => None 45 | } 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /internal/src/main/scala/lotos/internal/testing/lts.scala: -------------------------------------------------------------------------------- 1 | package lotos.internal.testing 2 | 3 | import cats.Functor 4 | import cats.effect.Sync 5 | import cats.implicits._ 6 | import lotos.internal.deepcopy._ 7 | import lotos.model.TestLog 8 | 9 | object lts { 10 | sealed trait CheckResult 11 | 12 | case class CheckSuccess(linearized: Vector[TestLog]) extends CheckResult 13 | case object CheckFailure extends CheckResult 14 | case object CheckNotStarted extends CheckResult 15 | 16 | // Check for the sequential consistency compliance 17 | def sequentially[F[_]](spec: Invoke[F], logs: Vector[Vector[TestLog]])(implicit F: Sync[F]): F[CheckResult] = { 18 | 19 | val eventsCounts = logs.map(_.size) 20 | val eventsTotal = eventsCounts.sum 21 | 22 | go(spec, 23 | List.iterate((0, 0), logs.size) { case (threadId, ind) => (threadId + 1, ind) }, 24 | Vector.empty, 25 | logs, 26 | eventsCounts, 27 | eventsTotal, 28 | considerTimes = false) 29 | } 30 | 31 | // Check for the linearizability compilance 32 | def linearizable[F[_]](spec: Invoke[F], logs: Vector[Vector[TestLog]])(implicit F: Sync[F]): F[CheckResult] = { 33 | val eventsCounts = logs.map(_.size) 34 | val eventsTotal = eventsCounts.sum 35 | 36 | go(spec, 37 | List.iterate((0, 0), logs.size) { case (threadId, ind) => (threadId + 1, ind) }, 38 | Vector.empty, 39 | logs, 40 | eventsCounts, 41 | eventsTotal, 42 | considerTimes = true) 43 | } 44 | 45 | private def go[F[_]: Sync](spec: Invoke[F], 46 | candidates: List[(Int, Int)], 47 | explanation: Vector[TestLog], 48 | logs: Vector[Vector[TestLog]], 49 | eventsCounts: Vector[Int], 50 | eventsTotal: Long, 51 | considerTimes: Boolean): F[CheckResult] = { 52 | if (explanation.size == eventsTotal) { 53 | (CheckSuccess(explanation): CheckResult).pure[F] 54 | } else { 55 | val filteredCandidates = if (considerTimes) { 56 | val maxEnding = 57 | candidates.map { case (threadId, order) => logs(threadId)(order) }.minBy(_.resp.timestamp).resp.timestamp 58 | candidates.zipWithIndex.filter { 59 | case ((threadId, order), _) => logs(threadId)(order).call.timestamp < maxEnding 60 | } 61 | } else candidates.zipWithIndex 62 | filteredCandidates 63 | .map { 64 | case ((threadId, order), candidateId) => 65 | for { 66 | specCopy <- deepCopyF(spec) 67 | log = logs(threadId)(order) 68 | moveForward <- validate(specCopy, logs(threadId)(order)) 69 | result <- if (moveForward) { 70 | val newCandidates = 71 | if (order < eventsCounts(threadId) - 1) 72 | candidates.updated(candidateId, (threadId, order + 1)) 73 | else candidates.patch(candidateId, Nil, 1) 74 | go(specCopy, newCandidates, explanation :+ log, logs, eventsCounts, eventsTotal, considerTimes) 75 | } else (CheckFailure: CheckResult).pure[F] 76 | } yield result 77 | } 78 | .collectFirstSomeM[F, CheckResult](testAction => 79 | testAction.map { 80 | case x @ CheckSuccess(_) => Some(x) 81 | case _ => None 82 | }) 83 | .map(_.getOrElse(CheckFailure)) 84 | } 85 | } 86 | 87 | private def validate[F[_]: Functor](spec: Invoke[F], event: TestLog): F[Boolean] = 88 | spec 89 | .invokeWithSeeds(event.call.methodName, event.call.paramSeeds) 90 | .map(specEvent => TestLog.catsEq.eqv(specEvent, event)) 91 | } 92 | -------------------------------------------------------------------------------- /testing/src/main/scala/lotos/testing/LotosTest.scala: -------------------------------------------------------------------------------- 1 | package lotos.testing 2 | 3 | import java.nio.file.Paths 4 | 5 | import cats.Parallel 6 | import cats.effect.{Concurrent, ContextShift, IO, Sync, Timer} 7 | import cats.implicits._ 8 | import lotos.model._ 9 | import lotos.internal.testing._ 10 | import lotos.internal.testing.lts._ 11 | import lotos.macros.TestConstructor 12 | import shapeless.HList 13 | 14 | object LotosTest { 15 | def in[F[_]]: Runner[F] = 16 | new Runner[F] 17 | 18 | def forSpec[Impl, Methods <: HList](spec: SpecT[Impl, Methods], cfg: TestConfig, consistency: Consistency)( 19 | implicit timer: Timer[IO] 20 | ): IO[TestResult] = 21 | macro TestConstructor.constructIO[Impl, Methods] 22 | 23 | class Runner[F[_]] { 24 | def forSpec[Impl, Methods <: HList](spec: SpecT[Impl, Methods], cfg: TestConfig, consistency: Consistency)( 25 | cs: ContextShift[F] 26 | )(implicit F: Concurrent[F], timer: Timer[F]): F[TestResult] = 27 | macro TestConstructor.constructF[F, Impl, Methods] 28 | } 29 | 30 | def run[F[_]: Parallel: Timer](cfg: TestConfig, invoke: Invoke[F], consistency: Consistency)( 31 | cs: ContextShift[F] 32 | )(implicit F: Concurrent[F]): F[TestResult] = { 33 | val testRun = TestRunImpl(invoke)(cs) 34 | val scenarios: List[Scenario] = 35 | List.fill(cfg.scenarioCount)(Scenario.gen(invoke.methods, cfg.parallelism, cfg.scenarioLength)) 36 | 37 | scenarios.zipWithIndex 38 | .map { 39 | case (scenario, ind) => 40 | val iterations = List 41 | .fill(cfg.scenarioRepetition) { 42 | for { 43 | logs <- testRun.run(scenario, cfg.operationTimeout) 44 | outcome <- if (logs.exists(_.length != cfg.scenarioLength)) (CheckNotStarted: CheckResult).pure[F] 45 | else 46 | consistency match { 47 | case Consistency.sequential => lts.sequentially(invoke, logs) 48 | case Consistency.linearizable => lts.linearizable(invoke, logs) 49 | } 50 | } yield (logs, outcome) 51 | } 52 | 53 | for { 54 | _ <- F.delay(println(s"Testing scenario ${ind + 1}")) 55 | scenarioOutcome <- iterations.collectFirstSomeM[F, TestResult](testAction => 56 | testAction.map { 57 | case (_, CheckSuccess(_)) => None 58 | case (logs, CheckFailure) => Some(TestFailure(logs)) 59 | case (logs, CheckNotStarted) => Some(TestTimeout(logs)) 60 | } 61 | ) 62 | } yield scenarioOutcome 63 | } 64 | .collectFirstSomeM[F, TestResult](identity) 65 | .flatMap { 66 | case Some(failure @ TestFailure(history)) => 67 | F.delay(println("Test failed")) *> onFailure(history, cfg).as(failure) 68 | case Some(crash @ TestTimeout(history)) => 69 | F.delay(println(s"Test timeouted")) *> onFailure(history, cfg).as(crash) 70 | case _ => 71 | F.delay(println("Test succeeded")).as(TestSuccess) 72 | } 73 | } 74 | 75 | def onFailure[F[_]](logs: Vector[Vector[TestLog]], cfg: TestConfig)(implicit F: Sync[F]): F[Unit] = 76 | cfg.failureFormat match { 77 | case FailureFormat.Console => F.delay(println(PrintLogs.prettyString(logs, cfg.scenarioLength))) 78 | case FailureFormat.Diagram(fileName) => 79 | F.delay(println(s"Writing logs to ${Paths.get(fileName).toAbsolutePath}")) *> PrintLogs 80 | .diagram[F](logs, cfg.scenarioLength, fileName) 81 | case FailureFormat.Both(fileName) => 82 | for { 83 | _ <- F.delay(println(s"Writing logs to $fileName")) 84 | _ <- F.delay(println(PrintLogs.prettyString(logs, cfg.scenarioLength))) 85 | _ <- PrintLogs.diagram[F](logs, cfg.scenarioLength, fileName) 86 | } yield () 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /model/src/main/scala/lotos/model/PrintLogs.scala: -------------------------------------------------------------------------------- 1 | package lotos.model 2 | 3 | import java.nio.file.{Files, Paths} 4 | 5 | import cats.effect.{IO, Sync} 6 | 7 | object PrintLogs { 8 | case class LogsWithTimes(logs: Vector[Vector[String]], maxLogLength: Int) 9 | 10 | private def logsWithTimes(logs: Vector[Vector[TestLog]], scenarioLength: Int): LogsWithTimes = { 11 | val sortedTimes = logs 12 | .flatMap(_.flatMap(event => List(event.call.timestamp, event.resp.timestamp))) 13 | .sorted 14 | .zipWithIndex 15 | 16 | val maxTime = sortedTimes.length 17 | 18 | val timesMapping = 19 | sortedTimes.toMap 20 | .mapValues(_.toLong) 21 | 22 | val maxLogLength = logs.flatMap(_.map(_.show(maxTime, maxTime).length)).max 23 | 24 | LogsWithTimes( 25 | logs = logs 26 | .map { column => 27 | column 28 | .map(event => { 29 | val str = 30 | event.show(timesMapping(event.call.timestamp), timesMapping(event.resp.timestamp)) 31 | s""" $str${" " * (maxLogLength - str.length)} """ 32 | }) 33 | }, 34 | maxLogLength = maxLogLength 35 | ) 36 | } 37 | 38 | def prettyString(rawLogs: Vector[Vector[TestLog]], scenarioLength: Int): String = { 39 | val LogsWithTimes(logs, maxLogLength) = logsWithTimes(rawLogs, scenarioLength) 40 | val emptyLog = " " * (maxLogLength + 2) 41 | logs 42 | .fold(Vector.fill(scenarioLength)("")) { 43 | case (l1, l2) => l1.zipAll(l2, emptyLog, emptyLog).map { case (s1, s2) => s"$s1|$s2" } 44 | } 45 | .mkString("\n") 46 | } 47 | 48 | def diagram[F[_]](rawLogs: Vector[Vector[TestLog]], scenarioLength: Int, fileName: String)( 49 | implicit F: Sync[F] 50 | ): F[Unit] = { 51 | val LogsWithTimes(logs, maxLogLength) = logsWithTimes(rawLogs, scenarioLength) 52 | val diagramContent = htmlDiagram() 53 | F.delay(Files.write(Paths.get(fileName), diagramContent.getBytes())) 54 | } 55 | 56 | private def htmlDiagram(): String = 57 | """| 58 | | 59 | | 60 | | 112 | | 113 | | 114 | | 115 | |
116 | |
117 | |
118 | |

Info about other step and more information and more and more and more and i aint gonna stop

119 | |
120 | | 121 | |
122 | |
123 | |
124 | |

Information about step

125 | |
126 | |

Info about other step and more information and more and more and more and i aint gonna stop

127 | |
128 | | 129 | |
130 | |
131 | | 132 | | 133 | |""".stripMargin 134 | } 135 | -------------------------------------------------------------------------------- /macros/src/main/scala/lotos/macros/TestConstructor.scala: -------------------------------------------------------------------------------- 1 | package lotos.macros 2 | 3 | import cats.effect.{Concurrent, ContextShift, IO, Timer} 4 | import cats.instances.either._ 5 | import cats.instances.list._ 6 | import cats.syntax.traverse._ 7 | import lotos.model._ 8 | import lotos.internal.testing.Invoke 9 | import lotos.model.TestResult 10 | import shapeless.{HList, HNil} 11 | 12 | import scala.reflect.macros.blackbox 13 | 14 | class TestConstructor(val c: blackbox.Context) extends ShapelessMacros { 15 | import c.universe._ 16 | 17 | type WTTF[F[_]] = WeakTypeTag[F[Unit]] 18 | 19 | def constructF[F[_]: WTTF, Impl: WeakTypeTag, Methods <: HList: WeakTypeTag]( 20 | spec: c.Expr[SpecT[Impl, Methods]], 21 | cfg: c.Expr[TestConfig], 22 | consistency: c.Expr[Consistency] 23 | )(cs: c.Expr[ContextShift[F]])(F: c.Expr[Concurrent[F]], timer: c.Expr[Timer[F]]): c.Expr[F[TestResult]] = { 24 | val invoke = constructInvoke[F, Impl, Methods](spec)(F, timer) 25 | 26 | val testRunTree = q"lotos.testing.LotosTest.run($cfg, $invoke, $consistency)($cs)" 27 | val checkedTree = typeCheckOrAbort(testRunTree) 28 | c.Expr(checkedTree) 29 | } 30 | 31 | def constructIO[Impl: WeakTypeTag, Methods <: HList: WeakTypeTag]( 32 | spec: c.Expr[SpecT[Impl, Methods]], 33 | cfg: c.Expr[TestConfig], 34 | consistency: c.Expr[Consistency] 35 | )(timer: c.Expr[Timer[IO]]): c.Expr[IO[TestResult]] = { 36 | val invoke = constructInvoke[IO, Impl, Methods](spec)(c.Expr(q"cats.effect.IO.ioEffect"), timer) 37 | val testRunTree = q""" 38 | import cats.effect.{ContextShift, Resource} 39 | import scala.concurrent.ExecutionContext 40 | import java.util.concurrent.Executors 41 | 42 | val csResource = Resource 43 | .make(IO(Executors.newFixedThreadPool($cfg.parallelism)))(ex => IO(ex.shutdown())) 44 | .map(ex => IO.contextShift(ExecutionContext.fromExecutor(ex))) 45 | 46 | csResource.use(cs => lotos.testing.LotosTest.run($cfg, $invoke, $consistency)(cs)) 47 | """ 48 | val checkedTree = typeCheckOrAbort(testRunTree) 49 | c.Expr(checkedTree) 50 | } 51 | 52 | private def constructInvoke[F[_]: WTTF, Impl: WeakTypeTag, Methods <: HList: WeakTypeTag]( 53 | spec: c.Expr[SpecT[Impl, Methods]], 54 | )(concF: c.Expr[Concurrent[F]], timerF: c.Expr[Timer[F]]): c.Expr[Invoke[F]] = { 55 | val FT = weakTypeOf[F[Unit]].typeConstructor 56 | val methodT = weakTypeOf[MethodT[Unit, HNil]].typeConstructor 57 | 58 | val implT = weakTypeOf[Impl] 59 | val specT = weakTypeOf[Methods] 60 | 61 | val implMethods = extractMethods(implT).toMap 62 | val specMethods: Vector[(String, NList[Type])] = hlistElements(specT).collect { 63 | case method if method.typeConstructor == methodT => 64 | method.typeArgs match { 65 | case List(name, params) => (unpackString(name), extractRecord(params)) 66 | case _ => abort(s"unexpected method definition $method") 67 | } 68 | }.toVector 69 | val methodTypeParams: Map[String, List[Type]] = hlistElements(specT).collect { 70 | case method if method.typeConstructor == methodT => 71 | method.typeArgs match { 72 | case List(name, params) => (unpackString(name), List(name, params)) 73 | case _ => abort(s"unexpected method definition $method") 74 | } 75 | }.toMap 76 | 77 | checkSpecOrAbort(specMethods, implMethods) 78 | 79 | def methodMatch(withSeeds: Boolean) = 80 | specMethods.map { 81 | case (mName, reversedParams) => 82 | val params = reversedParams.reverse 83 | val paramList = params.map { 84 | case (pName, tpe) => 85 | q"""${TermName(pName)} = { 86 | val paramGen = paramGens($pName).asInstanceOf[Gen[$tpe]] 87 | val param = paramGen.gen(seeds($pName)) 88 | showParams = showParams + ($pName -> paramGen.show(param)) 89 | param 90 | }""" 91 | } 92 | 93 | val seeds = 94 | if (withSeeds) q"" 95 | else 96 | q"""val seeds: Map[String, Long] = Map(..${params.map { 97 | case (pName, _) => q"$pName -> random.nextLong()" 98 | }})""" 99 | 100 | val methodInvoke = 101 | if (paramList.isEmpty) 102 | q"$concF.delay(impl.${TermName(mName)}.toString)" 103 | else 104 | q"$concF.delay(impl.${TermName(mName)}(..$paramList).toString)" 105 | 106 | val invokeWithTime = 107 | q""" 108 | $methodInvoke.flatMap(okResp => 109 | currentTime.map(endTime => 110 | MethodResp.Ok( 111 | result = okResp.toString, 112 | timestamp = endTime 113 | ))) 114 | .handleErrorWith(error => 115 | currentTime.map(endTime => 116 | MethodResp.Fail( 117 | error = error, 118 | timestamp = endTime 119 | ))) 120 | """ 121 | val invocation = 122 | if (withSeeds) invokeWithTime 123 | else 124 | q""" 125 | Concurrent.timeoutTo( 126 | $invokeWithTime, 127 | timeout, 128 | currentTime.map(endTime => 129 | MethodResp.Timeout( 130 | timestamp = endTime 131 | ))) 132 | """ 133 | cq"""${q"$mName"} => 134 | $seeds 135 | var showParams: Map[String, String] = Map.empty 136 | val methodT = 137 | SpecT.methods($spec)(method) 138 | .asInstanceOf[MethodT[..${methodTypeParams(mName)}]] 139 | val paramGens = MethodT.paramGens(methodT) 140 | val currentTime = $timerF.clock.monotonic(NANOSECONDS) 141 | for { 142 | startTime <- currentTime 143 | resp <- $invocation 144 | } yield TestLog( 145 | call = MethodCall( 146 | methodName = ${q"$mName"}, 147 | paramSeeds = seeds, 148 | params = showParams.toList.map{case (k, v) => k + "=" + v}.mkString(", "), 149 | timestamp = startTime 150 | ), 151 | resp = resp) 152 | """ 153 | } :+ cq"""_ => $concF.pure(TestLog(MethodCall("Unknown method", Map.empty, "", 0L), MethodResp.Ok("",0L))) """ 154 | 155 | val invokeTree = q""" 156 | import lotos.model._ 157 | 158 | import lotos.internal.testing._ 159 | import scala.util.Random 160 | import scala.concurrent.duration._ 161 | import cats.effect.Concurrent 162 | 163 | new Invoke[$FT] { 164 | private val random = new Random(System.currentTimeMillis()) 165 | private val impl = SpecT.construct($spec) 166 | 167 | def copy: Invoke[$FT] = this 168 | def invoke(method: String, timeout: FiniteDuration): ${appliedType(FT, typeOf[TestLog])} = 169 | method match { 170 | case ..${methodMatch(withSeeds = false)} 171 | } 172 | def invokeWithSeeds(method: String, 173 | seeds: Map[String, Long]): ${appliedType(FT, typeOf[TestLog])} = 174 | method match { 175 | case ..${methodMatch(withSeeds = true)} 176 | } 177 | def methods: Vector[String] = ${specMethods.map(_._1)} 178 | } 179 | """ 180 | val checkedTree = typeCheckOrAbort(invokeTree) 181 | c.Expr(checkedTree) 182 | } 183 | 184 | type SEither[A] = Either[String, A] 185 | 186 | private def checkSpecOrAbort( 187 | specMethods: Vector[(String, List[(String, Type)])], 188 | implMethods: Map[String, MethodDecl[Type]] 189 | ): Unit = { 190 | specMethods.foreach { 191 | case (name, args) => 192 | val specArgsMap = args.toMap 193 | val validation = for { 194 | (implArgLists, _) <- implMethods 195 | .get(name) 196 | .toRight(s"specified method `$name` does not exist in the implementation") 197 | _ <- implArgLists.flatten.traverse[SEither, (String, Type)] { 198 | case (implArgName, implArgType) => 199 | for { 200 | specArgType <- specArgsMap 201 | .get(implArgName) 202 | .toRight( 203 | s"argument `$implArgName` of method `$name` does not exist in the specification" 204 | ) 205 | _ <- Either.cond( 206 | specArgType =:= implArgType, 207 | (), 208 | s"argument `$implArgName` of method `$name` is declared to be `$specArgType` but `$implArgType` encountered in implementation" 209 | ) 210 | } yield (implArgName, implArgType) 211 | } 212 | } yield () 213 | validation.fold(abort, identity) 214 | } 215 | } 216 | } 217 | --------------------------------------------------------------------------------