diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..77eb0a7 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,11 @@ +## Maintainers + +* [Anvar Kiekbaev](https://github.com/chepiov) + +## Contributors +Contributors come in many forms. + +If you see your name on here, and do not want to be mentioned, you can open a PR to remove. + +* [Vasiliy Morkovkin](https://github.com/susliko) +* [Oleg Nizhnik](https://github.com/Odomontois) \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b0793f7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at a.kiekbaev@chepiov.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/src/main/scala/org/chepiov/tomodoro/actors/UserActor.scala b/src/main/scala/org/chepiov/tomodoro/actors/UserActor.scala index 0f23f4b..9064d03 100644 --- a/src/main/scala/org/chepiov/tomodoro/actors/UserActor.scala +++ b/src/main/scala/org/chepiov/tomodoro/actors/UserActor.scala @@ -103,7 +103,6 @@ class UserActor( case RecoveryCompleted => timerState(state.status) log.debug(s"[$chatId] Recovering completed. Current state: $state") - case s => log.debug(s"DAFUUUCK: $s") } private def timerState(status: Status): Unit = @@ -126,12 +125,12 @@ case object UserActor { def props( chatId: Long, - chat: ActorSelection, + messenger: ActorSelection, timeUnit: TimeUnit = MINUTES, defaultSettings: UserSettings = defaultUserSettings, snapshotInterval: Int = 1000 ): Props = - Props(new UserActor(chatId, chat, timeUnit, defaultSettings, snapshotInterval)) + Props(new UserActor(chatId, messenger, timeUnit, defaultSettings, snapshotInterval)) final case class CommandMsg(cmd: Command, ack: () => Unit) diff --git a/src/main/scala/org/chepiov/tomodoro/actors/UsersActor.scala b/src/main/scala/org/chepiov/tomodoro/actors/UsersActor.scala index c049808..ac22547 100644 --- a/src/main/scala/org/chepiov/tomodoro/actors/UsersActor.scala +++ b/src/main/scala/org/chepiov/tomodoro/actors/UsersActor.scala @@ -32,7 +32,7 @@ class UsersActor[F[_]: Effect](messenger: TSendMessage => F[Unit], activity: Sta case GetUser(chatId, ack) => val userActor = createUser(chatId) log.debug(s"[$chatId] [re]creating user: ${userActor.path}") - become(behavior(chatIdToUser + ((chatId, userActor)), userToChatId + ((userActor, chatId)))) + updateState(chatId, userActor, chatIdToUser, userToChatId) ack(userActor) } @@ -67,12 +67,20 @@ class UsersActor[F[_]: Effect](messenger: TSendMessage => F[Unit], activity: Sta override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => Restart } + + private[actors] def updateState( + chatId: Long, + userActor: ActorRef, + chatIdToUser: Map[Long, ActorRef], + userToChatId: Map[ActorRef, Long] + ): Unit = + become(behavior(chatIdToUser + ((chatId, userActor)), userToChatId + ((userActor, chatId)))) } case object UsersActor { - def props[F[_]: Effect](messenger: TSendMessage => F[Unit], stat: StateChangedEvent => F[Try[Unit]]): Props = - Props(new UsersActor(messenger, stat)) + def props[F[_]: Effect](messenger: TSendMessage => F[Unit], activity: StateChangedEvent => F[Try[Unit]]): Props = + Props(new UsersActor(messenger, activity)) final case class GetUser(chatId: Long, ack: ActorRef => Unit) } diff --git a/src/test/scala/org/chepiov/tomodoro/actors/MessengerActorSpec.scala b/src/test/scala/org/chepiov/tomodoro/actors/MessengerActorSpec.scala index 5cdd67b..3f69661 100644 --- a/src/test/scala/org/chepiov/tomodoro/actors/MessengerActorSpec.scala +++ b/src/test/scala/org/chepiov/tomodoro/actors/MessengerActorSpec.scala @@ -9,8 +9,14 @@ import org.chepiov.tomodoro.algebras.Telegram.TSendMessage import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} class MessengerActorSpec - extends TestKit(ActorSystem("test-system", ConfigFactory.load("application-persistence-test"))) with WordSpecLike - with Matchers with BeforeAndAfterAll with ImplicitSender { + extends TestKit( + ActorSystem( + "messenger-test-system", + ConfigFactory + .parseString(PersistenceSpec.config("messenger")) + .withFallback(ConfigFactory.load()) + ) + ) with WordSpecLike with Matchers with BeforeAndAfterAll with ImplicitSender with PersistenceSpec { override def afterAll: Unit = { TestKit.shutdownActorSystem(system) diff --git a/src/test/scala/org/chepiov/tomodoro/actors/PersistenceSpec.scala b/src/test/scala/org/chepiov/tomodoro/actors/PersistenceSpec.scala new file mode 100644 index 0000000..2ded88a --- /dev/null +++ b/src/test/scala/org/chepiov/tomodoro/actors/PersistenceSpec.scala @@ -0,0 +1,46 @@ +package org.chepiov.tomodoro.actors + +import java.io.IOException +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file._ + +import akka.testkit.TestKit +import org.scalatest.BeforeAndAfterAll + +@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) +trait PersistenceSpec { self: TestKit with BeforeAndAfterAll => + override def beforeAll(): Unit = { + List( + "akka.persistence.journal.leveldb.dir", + "akka.persistence.snapshot-store.local.dir" + ).filter { s => + val path = Paths.get(system.settings.config.getString(s)) + Files.exists(path) && Files.isDirectory(path) + }.foreach { s => + Files.walkFileTree( + Paths.get(system.settings.config.getString(s)), + new SimpleFileVisitor[Path] { + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + Files.deleteIfExists(file) + FileVisitResult.CONTINUE + } + override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { + Files.deleteIfExists(dir) + FileVisitResult.CONTINUE + } + } + ) + } + } +} + +object PersistenceSpec { + def config(dir: String): String = + s""" + |akka.persistence.journal.plugin = "akka.persistence.journal.leveldb" + |akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local" + |akka.persistence.journal.leveldb.dir = "target/$dir/journal" + |akka.persistence.snapshot-store.local.dir = "target/$dir/snapshots" + |akka.persistence.journal.leveldb.native = false + """.stripMargin +} diff --git a/src/test/scala/org/chepiov/tomodoro/actors/UserActorSpec.scala b/src/test/scala/org/chepiov/tomodoro/actors/UserActorSpec.scala index a2bf54d..6f7485b 100644 --- a/src/test/scala/org/chepiov/tomodoro/actors/UserActorSpec.scala +++ b/src/test/scala/org/chepiov/tomodoro/actors/UserActorSpec.scala @@ -1,8 +1,5 @@ package org.chepiov.tomodoro.actors -import java.io.IOException -import java.nio.file._ -import java.nio.file.attribute.BasicFileAttributes import java.time.OffsetDateTime import akka.actor.{ActorIdentity, ActorRef, ActorSelection, ActorSystem, Identify, Props} @@ -17,33 +14,15 @@ import scala.concurrent.duration._ @SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) class UserActorSpec - extends TestKit(ActorSystem("test-system", ConfigFactory.load("application-persistence-test"))) with WordSpecLike - with Matchers with BeforeAndAfterAll with ImplicitSender { - import UserActorSpec._ - - override def beforeAll(): Unit = { - List( - "akka.persistence.journal.leveldb.dir", - "akka.persistence.snapshot-store.local.dir" - ).filter { s => - val path = Paths.get(system.settings.config.getString(s)) - Files.exists(path) && Files.isDirectory(path) - }.foreach { s => - Files.walkFileTree( - Paths.get(system.settings.config.getString(s)), - new SimpleFileVisitor[Path] { - override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { - Files.deleteIfExists(file) - FileVisitResult.CONTINUE - } - override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { - Files.deleteIfExists(dir) - FileVisitResult.CONTINUE - } - } + extends TestKit( + ActorSystem( + "user-test-system", + ConfigFactory + .parseString(PersistenceSpec.config("user")) + .withFallback(ConfigFactory.load()) ) - } - } + ) with WordSpecLike with Matchers with BeforeAndAfterAll with ImplicitSender with PersistenceSpec { + import UserActorSpec._ override def afterAll: Unit = { TestKit.shutdownActorSystem(system) diff --git a/src/test/scala/org/chepiov/tomodoro/actors/UsersActorSpec.scala b/src/test/scala/org/chepiov/tomodoro/actors/UsersActorSpec.scala new file mode 100644 index 0000000..449650e --- /dev/null +++ b/src/test/scala/org/chepiov/tomodoro/actors/UsersActorSpec.scala @@ -0,0 +1,76 @@ +package org.chepiov.tomodoro.actors + +import java.util.concurrent.atomic.AtomicReference + +import akka.actor.{ActorIdentity, ActorRef, ActorSystem, Identify, Props} +import akka.testkit.{ImplicitSender, TestKit} +import cats.effect.IO +import com.typesafe.config.ConfigFactory +import org.chepiov.tomodoro.actors.UsersActor.GetUser +import org.chepiov.tomodoro.algebras.Telegram.TSendMessage +import org.chepiov.tomodoro.programs.UserActivity.StateChangedEvent +import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} + +import scala.util.{Success, Try} + +//noinspection AppropriateActorConstructorNotFound +@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements")) +class UsersActorSpec + extends TestKit( + ActorSystem( + "users-test-system", + ConfigFactory + .parseString(PersistenceSpec.config("users")) + .withFallback(ConfigFactory.load()) + ) + ) with WordSpecLike with Matchers with BeforeAndAfterAll with ImplicitSender with PersistenceSpec { + import UsersActorSpec._ + + override def afterAll: Unit = { + TestKit.shutdownActorSystem(system) + } + + val messenger: TSendMessage => IO[Unit] = _ => IO.unit + val activity: StateChangedEvent => IO[Try[Unit]] = _ => IO(Success(())) + + "UsersActor" should { + "create new user or return existing" in { + + val usersActor = system.actorOf(Props(classOf[TestUsersActor], testActor, messenger, activity)) + + // awaiting full creation for timing + system.actorSelection(usersActor.path) ! Identify(1) + expectMsg(ActorIdentity(1, Some(usersActor))) + + usersActor ! GetUser(1L, _ => ()) + val (chatId, userActor) = expectMsgType[(Long, ActorRef)] + chatId shouldBe 1L + + val hook = new AtomicReference[Option[ActorRef]](None) + usersActor ! GetUser(1L, ref => hook.set(Some(ref))) + + expectNoMessage() + awaitAssert(hook.get().isDefined) + + hook.get() shouldBe Some(userActor) + } + } +} + +case object UsersActorSpec { + class TestUsersActor( + probe: ActorRef, + messenger: TSendMessage => IO[Unit], + activity: StateChangedEvent => IO[Try[Unit]] + ) extends UsersActor[IO](messenger, activity) { + override def updateState( + chatId: Long, + userActor: ActorRef, + chatIdToUser: Map[Long, ActorRef], + userToChatId: Map[ActorRef, Long] + ): Unit = { + probe.tell((chatId, userActor), self) + super.updateState(chatId, userActor, chatIdToUser, userToChatId) + } + } +} diff --git a/src/test/scala/org/chepiov/tomodoro/interpreters/UserInterpreterSpec.scala b/src/test/scala/org/chepiov/tomodoro/interpreters/UserInterpreterSpec.scala index 4cff444..86d257d 100644 --- a/src/test/scala/org/chepiov/tomodoro/interpreters/UserInterpreterSpec.scala +++ b/src/test/scala/org/chepiov/tomodoro/interpreters/UserInterpreterSpec.scala @@ -5,7 +5,6 @@ import akka.testkit.{ImplicitSender, TestKit} import cats.Id import cats.effect.{ContextShift, IO} import cats.syntax.flatMap._ -import com.typesafe.config.ConfigFactory import io.chrisdavenport.log4cats.Logger import io.chrisdavenport.log4cats.slf4j.Slf4jLogger import org.chepiov.tomodoro.actors.UserActor.{CommandMsg, QueryMsg} @@ -24,8 +23,8 @@ import scala.concurrent.duration._ ) ) class UserInterpreterSpec - extends TestKit(ActorSystem("test-system", ConfigFactory.load("application-persistence-test"))) with WordSpecLike - with Matchers with BeforeAndAfterAll with ImplicitSender { + extends TestKit(ActorSystem("test-system")) with WordSpecLike with Matchers with BeforeAndAfterAll + with ImplicitSender { import UserInterpreterSpec._ override def afterAll: Unit = {