├── .gitignore ├── .travis.yml ├── LICENSE ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── readme.md └── src ├── main └── scala │ └── com │ └── github │ └── dnvriend │ └── HttpUtils.scala └── test ├── resources └── application.conf └── scala └── com └── test ├── TestSpec.scala ├── week1 ├── CaseClassesTest.scala ├── CollectionsTest.scala ├── ForExpressionTest.scala ├── FunctionTest.scala ├── FunctionalRandomGeneratorTest.scala ├── JsonTest.scala ├── MonadTest.scala └── PatternMatchTest.scala ├── week2 ├── FRPTest.scala ├── FunctionsAndStateTest.scala ├── LoopTest.scala ├── ObjectIdentityTest.scala ├── ObserverTest.scala ├── StreamsTest.scala ├── frp │ ├── BankAccount.scala │ ├── Signal.scala │ ├── StackableVariable.scala │ └── Var.scala ├── observer │ ├── BankAccount.scala │ ├── Consolidator.scala │ ├── Publisher.scala │ └── Subscriber.scala └── state │ └── BankAccount.scala ├── week3 ├── AdventureGameOneTest.scala ├── AdventureGameTwoTest.scala ├── AsyncAwaitTest.scala ├── ComposingFuturesTest.scala ├── FuturesTest.scala ├── LatencyAsAnEffectTest.scala └── PromiseTest.scala ├── week4 ├── EarthquakeTest.scala ├── IterableTest.scala ├── PromisesAndSubjectsTest.scala ├── RxOperatorsTest.scala ├── SubscriptionsTest.scala └── Usgs.scala ├── week5 ├── AsyncHttoClientTest.scala ├── BankAccountTest.scala ├── CounterTest.scala ├── LinkCheckerTest.scala └── RunningWithTestProbeTest.scala └── week6 ├── AtLeastOnceDeliveryTest.scala ├── BlogPostTest.scala ├── DeathPactTest.scala ├── LinkCheckerDeathWatchTest.scala └── SupervisionTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | /RUNNING_PID 2 | /logs/ 3 | /project/*-shim.sbt 4 | /project/project/ 5 | /project/target/ 6 | /target/ 7 | /journal/ 8 | /snapshot/ 9 | .idea 10 | *.iml 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | sudo: false 3 | scala: 4 | - "2.11.6" 5 | jdk: 6 | - oraclejdk8 7 | branches: 8 | only: 9 | - master 10 | notifications: 11 | email: 12 | - dnvriend@gmail.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "reactive-programming" 2 | 3 | version := "1.0.0" 4 | 5 | scalaVersion := "2.11.7" 6 | 7 | resolvers += "dnvriend at bintray" at "http://dl.bintray.com/dnvriend/maven" 8 | 9 | libraryDependencies ++= { 10 | val akkaVersion = "2.4.1" 11 | Seq( 12 | "com.typesafe.akka" %% "akka-actor" % akkaVersion, 13 | "com.typesafe.akka" %% "akka-persistence" % akkaVersion, 14 | "com.github.dnvriend" %% "akka-persistence-inmemory" % "1.1.6", 15 | "com.ning" % "async-http-client" % "1.7.19", 16 | "org.jsoup" % "jsoup" % "1.8.1", 17 | "io.reactivex" %% "rxscala" % "0.25.0", 18 | "org.scala-lang.modules" %% "scala-async" % "0.9.5", 19 | "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test, 20 | "org.scalatest" %% "scalatest" % "2.2.4" % Test, 21 | "org.scalacheck" %% "scalacheck" % "1.12.2" % Test 22 | ) 23 | } 24 | 25 | fork in Test := true 26 | 27 | parallelExecution := false 28 | 29 | 30 | licenses +=("Apache-2.0", url("http://opensource.org/licenses/apache2.0.php")) 31 | 32 | // enable scala code formatting // 33 | import scalariform.formatter.preferences._ 34 | 35 | scalariformSettings 36 | 37 | ScalariformKeys.preferences := ScalariformKeys.preferences.value 38 | .setPreference(AlignSingleLineCaseStatements, true) 39 | .setPreference(AlignSingleLineCaseStatements.MaxArrowIndent, 100) 40 | .setPreference(DoubleIndentClassDeclaration, true) 41 | .setPreference(RewriteArrowSymbols, true) 42 | 43 | // enable updating file headers // 44 | import de.heikoseeberger.sbtheader.license.Apache2_0 45 | 46 | headers := Map( 47 | "scala" -> Apache2_0("2015", "Dennis Vriend"), 48 | "conf" -> Apache2_0("2015", "Dennis Vriend", "#") 49 | ) 50 | 51 | enablePlugins(AutomateHeaderPlugin) -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "sonatype-releases" at "https://oss.sonatype.org/content/repositories/releases/" 2 | 3 | resolvers += "bintray-sbt-plugin-releases" at "http://dl.bintray.com/content/sbt/sbt-plugin-releases" 4 | 5 | // to show a dependency graph 6 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.8.0") 7 | 8 | // to format scala source code 9 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 10 | 11 | // enable updating file headers eg. for copyright 12 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.5.0") -------------------------------------------------------------------------------- /src/main/scala/com/github/dnvriend/HttpUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnvriend 18 | 19 | import java.util.concurrent.Executor 20 | 21 | import com.ning.http.client.AsyncHttpClient 22 | import org.jsoup.Jsoup 23 | import org.jsoup.nodes.Document 24 | import org.jsoup.select.Elements 25 | import scala.collection.JavaConverters._ 26 | import scala.concurrent.{ Promise, ExecutionContext, Future } 27 | import scala.util.Try 28 | 29 | object HttpClient { 30 | def apply(): AsyncHttpClient = new AsyncHttpClient 31 | 32 | implicit class HttpClientToScala(client: AsyncHttpClient) { 33 | def get(url: String)(implicit ec: Executor): Future[String] = { 34 | val f = client.prepareGet(url).execute 35 | val p = Promise[String]() 36 | f.addListener(new Runnable { 37 | override def run(): Unit = { 38 | val response = f.get 39 | if (response.getStatusCode < 400) 40 | p.success(response.getResponseBodyExcerpt(131072)) 41 | else p.failure(new RuntimeException(s"BadStatus: ${response.getStatusCode}")) 42 | } 43 | }, ec) 44 | p.future 45 | } 46 | } 47 | } 48 | 49 | object HttpUtils { 50 | implicit class FindLinksFuture(self: Future[String])(implicit ec: ExecutionContext) { 51 | def links: Future[Option[Iterator[String]]] = 52 | self.map(body ⇒ findLinks(body)) 53 | } 54 | 55 | def findLinks(body: String): Option[Iterator[String]] = 56 | Try(Jsoup.parse(body)).map { (document: Document) ⇒ 57 | val links: Elements = document.select("a[href]") 58 | for (link ← links.iterator().asScala; if link.absUrl("href").startsWith("http://")) yield link.absUrl("href") 59 | }.toOption 60 | } 61 | -------------------------------------------------------------------------------- /src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Dennis Vriend 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | akka { 16 | stdout-loglevel = debug // defaults to WARNING can be disabled with off. The stdout-loglevel is only in effect during system startup and shutdown 17 | log-dead-letters-during-shutdown = on 18 | loglevel = debug 19 | log-dead-letters = on 20 | log-config-on-start = off // Log the complete configuration at INFO level when the actor system is started 21 | 22 | actor { 23 | debug { 24 | receive = on // log all messages sent to an actor if that actors receive method is a LoggingReceive 25 | autoreceive = on // log all special messages like Kill, PoisoffPill etc sent to all actors 26 | lifecycle = on // log all actor lifecycle events of all actors 27 | fsm = on // enable logging of all events, transitioffs and timers of FSM Actors that extend LoggingFSM 28 | event-stream = on // enable logging of subscriptions (subscribe/unsubscribe) on the ActorSystem.eventStream 29 | } 30 | } 31 | 32 | stream { 33 | materializer { 34 | debug-logging = on // Enable additional troubleshooting logging at DEBUG log level 35 | } 36 | } 37 | persistence { 38 | journal.plugin = "inmemory-journal" 39 | snapshot-store.plugin = "inmemory-snapshot-store" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/scala/com/test/TestSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test 18 | 19 | import java.io.IOException 20 | import java.util.UUID 21 | 22 | import akka.actor.{ ActorRef, ActorSystem, PoisonPill } 23 | import akka.event.{ Logging, LoggingAdapter } 24 | import akka.testkit.TestProbe 25 | import akka.util.Timeout 26 | import org.scalatest.concurrent.{ Eventually, ScalaFutures } 27 | import org.scalatest.exceptions.TestFailedException 28 | import org.scalatest._ 29 | import rx.lang.scala._ 30 | 31 | import scala.concurrent.duration._ 32 | import scala.concurrent.{ ExecutionContextExecutor, Future } 33 | import scala.util.{ Random ⇒ Rnd, Try } 34 | 35 | object Random { 36 | def apply(): Rnd = new Rnd() 37 | } 38 | 39 | trait TestSpec extends FlatSpec with Matchers with ScalaFutures with TryValues with OptionValues with Eventually with BeforeAndAfterAll { 40 | implicit val system: ActorSystem = ActorSystem("test") 41 | implicit val ec: ExecutionContextExecutor = system.dispatcher 42 | val log: LoggingAdapter = Logging(system, this.getClass) 43 | implicit val pc: PatienceConfig = PatienceConfig(timeout = 50.seconds) 44 | implicit val timeout = Timeout(50.seconds) 45 | 46 | override protected def afterAll(): Unit = { 47 | system.terminate() 48 | } 49 | 50 | /** 51 | * TestKit-based probe which allows sending, reception and reply. 52 | */ 53 | def probe: TestProbe = TestProbe() 54 | 55 | /** 56 | * Returns a random UUID 57 | */ 58 | def randomId = UUID.randomUUID.toString.take(5) 59 | 60 | /** 61 | * Sends the PoisonPill command to an actor and waits for it to die 62 | */ 63 | def cleanup(actors: ActorRef*): Unit = { 64 | actors.foreach { (actor: ActorRef) ⇒ 65 | actor ! PoisonPill 66 | probe watch actor 67 | } 68 | } 69 | 70 | implicit class PimpedByteArray(self: Array[Byte]) { 71 | def getString: String = new String(self) 72 | } 73 | 74 | implicit class PimpedFuture[T](self: Future[T]) { 75 | def toTry: Try[T] = Try(self.futureValue) 76 | } 77 | 78 | implicit class PimpedObservable[T](self: Observable[T]) { 79 | def waitFor: Unit = { 80 | self.toBlocking.toIterable.last 81 | } 82 | } 83 | 84 | implicit class MustBeWord[T](self: T) { 85 | def mustBe(pf: PartialFunction[T, Unit]): Unit = 86 | if (!pf.isDefinedAt(self)) throw new TestFailedException("Unexpected: " + self, 0) 87 | } 88 | 89 | object Socket { def apply() = new Socket } 90 | class Socket { 91 | def readFromMemory: Future[Array[Byte]] = Future { 92 | Thread.sleep(100) // sleep 100 millis 93 | "fromMemory".getBytes 94 | } 95 | 96 | def send(payload: Array[Byte], from: String, failed: Boolean): Future[Array[Byte]] = 97 | if (failed) Future.failed(new IOException(s"Network error: $from")) 98 | else { 99 | Future { 100 | Thread.sleep(250) // sleep 250 millis, not real life time, but hey 101 | s"${payload.getString}->$from".getBytes 102 | } 103 | } 104 | 105 | def sendToEurope(payload: Array[Byte], failed: Boolean = false): Future[Array[Byte]] = 106 | send(payload, "fromEurope", failed) 107 | 108 | def sendToUsa(payload: Array[Byte], failed: Boolean = false): Future[Array[Byte]] = 109 | send(payload, "fromUsa", failed) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/CaseClassesTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.TestSpec 20 | 21 | class CaseClassesTest extends TestSpec { 22 | 23 | // Case Classes are used to conveniently store and 24 | // match on the contents of a class. You can construct 25 | // them without using the 'new' keyword. 26 | 27 | case class Calculator(brand: String, model: String) 28 | 29 | "case classes" should "be created easily" in { 30 | Calculator("hp", "20b") shouldBe Calculator("hp", "20b") 31 | } 32 | 33 | // case classes have equality, hashcode and nice toString methods 34 | // based on the constructor arguments 35 | 36 | "case classes" should "have nice toString method" in { 37 | Calculator("hp", "20b").toString shouldBe "Calculator(hp,20b)" 38 | } 39 | 40 | it should "have nice equality method" in { 41 | val hp20b = Calculator("hp", "20b") 42 | val hp20B = Calculator("hp", "20b") 43 | hp20b shouldBe hp20B 44 | } 45 | 46 | // case classes can have methods just like normal classes. 47 | 48 | case class CalulcatorWithMethod(brand: String, model: String) { 49 | def hello: String = s"Hello I am the $brand/$model" 50 | } 51 | 52 | "case classes" should "be able to have methods" in { 53 | CalulcatorWithMethod("hp", "20b").hello shouldBe "Hello I am the hp/20b" 54 | } 55 | 56 | // case classes are designed to be used with pattern matching. 57 | 58 | trait CalcType 59 | case object Financial extends CalcType 60 | case object Scientific extends CalcType 61 | case object Business extends CalcType 62 | case class Unknown(message: String) extends CalcType 63 | 64 | def calcType(calc: Calculator): CalcType = calc match { 65 | case Calculator("hp", "20B") ⇒ Financial 66 | case Calculator("hp", "48G") ⇒ Scientific 67 | case Calculator("hp", "30B") ⇒ Business 68 | case Calculator(brand, model) ⇒ Unknown(s"Calculator $brand, $model is of unknown type") 69 | } 70 | 71 | "case classes" should "be used with pattern matching" in { 72 | calcType(Calculator("hp", "20B")) shouldBe Financial 73 | calcType(Calculator("hp", "48G")) shouldBe Scientific 74 | calcType(Calculator("hp", "30B")) shouldBe Business 75 | calcType(Calculator("hp", "NW280AA")) shouldBe Unknown("Calculator hp, NW280AA is of unknown type") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/CollectionsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.TestSpec 20 | 21 | class CollectionsTest extends TestSpec { 22 | 23 | /* scala has the following collection hierarchy 24 | 25 | Traversable 26 | | 27 | | 28 | Iterable 29 | | 30 | +------------------+--------------------+ 31 | Map Set Seq 32 | | | | 33 | | +----+----+ +-----+------+ 34 | Sorted Map SortedSet BitSet IndexedSeq LineairSeq 35 | */ 36 | 37 | "IndexedSeq" should "have map, flatMap and Filter" in { 38 | // IndexedSeq collections are optimized for contant-time or near constant-time access 39 | // and length computations. It provides random access and updates in constant time, as 40 | // well as very fast append and prepend. 41 | val xs: Vector[Int] = Vector(1, 2, 3, 4, 5) 42 | xs.head shouldBe 1 43 | xs.last shouldBe 5 44 | xs.drop(1) shouldBe Vector(2, 3, 4, 5) 45 | xs.drop(1).dropRight(1) shouldBe Vector(2, 3, 4) 46 | xs.filter(_ % 2 == 0) shouldBe Vector(2, 4) 47 | xs.filterNot(_ % 2 == 0) shouldBe Vector(1, 3, 5) 48 | xs.map(_ * 2).find(_ == 5) shouldBe None 49 | xs.map(_ * 2).find(_ == 10) shouldBe Some(10) 50 | 51 | xs.sum shouldBe 1 + 2 + 3 + 4 + 5 52 | 53 | val xx = for (x ← xs) yield x * 2 54 | xx shouldBe Vector(2, 4, 6, 8, 10) 55 | 56 | // the Vector can also be created like this 57 | val xy = for (x ← 1 to 5) yield x * 2 58 | xy shouldBe Vector(2, 4, 6, 8, 10) 59 | 60 | val xz = for (x ← 1 to 5 if x > 2) yield x 61 | xz shouldBe Vector(3, 4, 5) 62 | 63 | xs.foldLeft(1) { _ * _ } shouldBe 120 64 | } 65 | 66 | "Arrays" should "have map, flatMap and Filter" in { 67 | // an Array is a *mutable* indexed collection of values, and is 100% 68 | // compatible with Java's Array, the T[] 69 | val xs: Array[Int] = Array(1, 2, 3, 4, 5) 70 | xs.head shouldBe 1 71 | xs.last shouldBe 5 72 | xs.drop(1) shouldBe Array(2, 3, 4, 5) 73 | xs.drop(1).dropRight(1) shouldBe Array(2, 3, 4) 74 | xs.filter(_ % 2 == 0) shouldBe Array(2, 4) 75 | xs.filterNot(_ % 2 == 0) shouldBe Array(1, 3, 5) 76 | xs.map(_ * 2).find(_ == 5) shouldBe None 77 | xs.map(_ * 2).find(_ == 10) shouldBe Some(10) 78 | 79 | xs.sum shouldBe 1 + 2 + 3 + 4 + 5 80 | 81 | val xx = for (x ← xs) yield x * 2 82 | xx shouldBe Array(2, 4, 6, 8, 10) 83 | 84 | val xz = for (x ← 1 to 5 if x > 2) yield x 85 | xz shouldBe Array(3, 4, 5) 86 | 87 | xs.foldLeft(1) { _ * _ } shouldBe 120 88 | } 89 | 90 | "List" should "have map, flatMap and Filter" in { 91 | // Lists are optimized for Sequential Scan (it is a LineairSeq) 92 | val xs: List[Int] = List(1, 2, 3, 4, 5) 93 | xs.head shouldBe 1 94 | xs.last shouldBe 5 95 | xs.drop(1) shouldBe List(2, 3, 4, 5) 96 | xs.drop(1).dropRight(1) shouldBe List(2, 3, 4) 97 | xs.filter(_ % 2 == 0) shouldBe List(2, 4) 98 | xs.filterNot(_ % 2 == 0) shouldBe List(1, 3, 5) 99 | xs.map(_ * 2).find(_ == 5) shouldBe None 100 | xs.map(_ * 2).find(_ == 10) shouldBe Some(10) 101 | 102 | xs.sum shouldBe 1 + 2 + 3 + 4 + 5 103 | 104 | val xx = for (x ← xs) yield x * 2 105 | xx shouldBe List(2, 4, 6, 8, 10) 106 | 107 | val xz = for (x ← 1 to 5 if x > 2) yield x 108 | xz shouldBe List(3, 4, 5) 109 | 110 | xs.foldLeft(1) { _ * _ } shouldBe 120 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/ForExpressionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.TestSpec 20 | 21 | class ForExpressionTest extends TestSpec { 22 | case class Person(name: String, isMale: Boolean, children: Person*) 23 | 24 | val lara = Person("Lara", isMale = false) 25 | val bob = Person("Bob", isMale = true) 26 | val julie = Person("Julie", isMale = false, lara, bob) 27 | val persons = List(lara, bob, julie) 28 | 29 | "for expression" should "query users" in { 30 | // find out the names of all pairs of mothers and their children 31 | // in the list persons 32 | 33 | // persons is a generator, thus for each person 34 | // test if the person is a male 35 | // if not, loop over children 36 | // yield a pair which contains the name of the mother 37 | // and the name of the child 38 | type MotherAndChildName = (String, String) 39 | val xs: List[MotherAndChildName] = 40 | for { 41 | p ← persons 42 | if !p.isMale 43 | c ← p.children 44 | } yield (p.name, c.name) 45 | xs shouldBe List(("Julie", "Lara"), ("Julie", "Bob")) 46 | } 47 | 48 | case class Book(title: String, authors: String*) 49 | val books: List[Book] = 50 | List( 51 | Book( 52 | "Structure and Interpretation of Computer Programs", 53 | "Abelson, Harold", "Sussman, Gerald J." 54 | ), 55 | Book( 56 | "Principles of Compiler Design", 57 | "Aho, Alfred", "Ullman, Jeffrey" 58 | ), 59 | Book( 60 | "Programming in Modula-2", 61 | "Wirth, Niklaus" 62 | ), 63 | Book( 64 | "Elements of ML Programming", 65 | "Ullman, Jeffrey" 66 | ), 67 | Book( 68 | "The Java Language Specification", "Gosling, James", 69 | "Joy, Bill", "Steele, Guy", "Bracha, Gilad" 70 | ) 71 | ) 72 | 73 | it should "find the title of all books whose author's last name is Gosling" in { 74 | val result = 75 | for { 76 | b ← books 77 | a ← b.authors 78 | if a.startsWith("Gosling") 79 | } yield b.title 80 | 81 | result shouldBe List("The Java Language Specification") 82 | } 83 | 84 | it should "find the titles of all books that have the string 'Program' in their title" in { 85 | val result = 86 | for { 87 | b ← books 88 | if b.title.indexOf("Program") >= 0 89 | } yield b.title 90 | 91 | result shouldBe List( 92 | "Structure and Interpretation of Computer Programs", 93 | "Programming in Modula-2", 94 | "Elements of ML Programming" 95 | ) 96 | } 97 | 98 | it should "find the names of all authors that have written at least two books in the database" in { 99 | val result = 100 | for { 101 | b1 ← books 102 | b2 ← books 103 | if b1 != b2 104 | a1 ← b1.authors 105 | a2 ← b2.authors 106 | if a1 == a2 107 | } yield a1 108 | 109 | result shouldBe List("Ullman, Jeffrey", "Ullman, Jeffrey") 110 | 111 | // let's remove the double entry 112 | def removeDuplicates(xs: List[String]): List[String] = xs match { 113 | case Nil ⇒ xs 114 | case head :: tail ⇒ 115 | head :: removeDuplicates( 116 | for { 117 | x ← tail 118 | if x != xs.head 119 | } yield x 120 | ) 121 | } 122 | removeDuplicates(result) shouldBe List("Ullman, Jeffrey") 123 | } 124 | 125 | it should "allow for nested for expressions" in { 126 | val xs = List(1, 2, 3) 127 | val xy = List("1", "2", "3") 128 | 129 | val result: List[(Int, Int)] = for { 130 | x ← xs 131 | y ← for (y ← xy) yield y.toInt 132 | if x <= 2 && y <= 2 133 | } yield (x, y) 134 | 135 | result shouldBe List((1, 1), (1, 2), (2, 1), (2, 2)) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/FunctionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.TestSpec 20 | 21 | class FunctionTest extends TestSpec { 22 | 23 | "Scala function literal" should "be of the following types" in { 24 | // all three are the same 25 | val f0_1: Function0[Unit] = new Function0[Unit] { 26 | override def apply(): Unit = Unit 27 | } 28 | val f0_2: Function0[Unit] = () ⇒ Unit 29 | val f0_3: Unit = () ⇒ Unit 30 | 31 | // all three are the same 32 | val f1_1: Function1[Unit, Unit] = new Function1[Unit, Unit] { 33 | override def apply(x: Unit): Unit = Unit 34 | } 35 | val f1_2: Function1[Unit, Unit] = (x: Unit) ⇒ Unit 36 | val f1_3: (Unit ⇒ Unit) = (x: Unit) ⇒ Unit 37 | 38 | // all three are the same 39 | val f2_1: Function1[Int, Unit] = new Function1[Int, Unit] { 40 | override def apply(x: Int): Unit = Unit 41 | } 42 | val f2_2: Function1[Int, Unit] = (x: Int) ⇒ Unit 43 | val f2_3: (Int ⇒ Unit) = (x: Int) ⇒ Unit 44 | 45 | // all three are the same 46 | val f3_1: Function[Int, Int] = new Function1[Int, Int] { 47 | override def apply(x: Int): Int = x 48 | } 49 | val f3_2: Function1[Int, Int] = (x: Int) ⇒ x 50 | val f3_3: Int ⇒ Int = (x: Int) ⇒ x 51 | 52 | // all three are the same 53 | val f4_1: Function2[Int, Int, Unit] = new Function2[Int, Int, Unit] { 54 | override def apply(x: Int, y: Int): Unit = Unit 55 | } 56 | val f4_2: Function2[Int, Int, Unit] = (x: Int, y: Int) ⇒ Unit 57 | val f4_3: (Int, Int) ⇒ Unit = (x: Int, y: Int) ⇒ Unit 58 | 59 | // all three are the same 60 | val f5_1: Function2[Int, Int, Int] = new Function2[Int, Int, Int] { 61 | override def apply(x: Int, y: Int): Int = x + y 62 | } 63 | val f5_2: Function2[Int, Int, Int] = (x: Int, y: Int) ⇒ x + y 64 | val f5_3: (Int, Int) ⇒ Int = (x: Int, y: Int) ⇒ x + y 65 | 66 | // all three are the same 67 | val f6_1: Function3[Int, Int, Int, Unit] = new Function3[Int, Int, Int, Unit] { 68 | override def apply(x: Int, y: Int, z: Int): Unit = Unit 69 | } 70 | val f6_2: Function3[Int, Int, Int, Unit] = (x: Int, y: Int, z: Int) ⇒ Unit 71 | val f6_3: (Int, Int, Int) ⇒ Unit = (x: Int, y: Int, z: Int) ⇒ Unit 72 | } 73 | 74 | "Functions" should "be able to be applied" in { 75 | // A function, like the ones above, can be applied. What does that mean? 76 | // A function like val `f = (x: Int) => x` can be evaluated. This 77 | // can be done by applying a value to the function. 78 | 79 | // when you evaluate a function by applying a value to that function 80 | // it looks a lot like math: 81 | 82 | val f: Int ⇒ Int = (x: Int) ⇒ x 83 | f(3) shouldBe 3 84 | 85 | // you can skip the type part if you wish: 86 | 87 | val g = (x: Int) ⇒ x 88 | g(3) shouldBe 3 89 | } 90 | 91 | it should "look like math" in { 92 | val f = (x: Int) ⇒ x * x 93 | f(2) shouldBe 4 94 | 95 | val g = (x: Int) ⇒ x + x 96 | g(2) shouldBe 4 97 | } 98 | 99 | it should "be applied in sequence" in { 100 | val f: Int ⇒ Int = (x: Int) ⇒ x * x 101 | val g: Int ⇒ Int = (x: Int) ⇒ x + x + x 102 | val h: Int ⇒ Int = (x: Int) ⇒ g(f(x)) 103 | val i: Int ⇒ Int = f andThen g 104 | val j: Int ⇒ Int = f compose g 105 | 106 | // functions can be nested, this infuences the sequence of 107 | // evaluation which is first 'f' andThen 'g': 108 | g(f(2)) shouldBe 12 109 | 110 | // the function 'h' does the same as the evaluation on line 91, 111 | // but it is defined as a function, thus we can easily evaluate it 112 | // by applying a number to it, like 2: 113 | h(2) shouldBe 12 114 | 115 | // the function 'i' composes these two functions. `andThen` is actually 116 | // a method on the 'f' function object, which is of type Function1. This composition 117 | // will first evaluate f and then evaluate g with the result of 'f', in which it does 118 | // the same as line 96: 119 | i(2) shouldBe 12 120 | 121 | // the evaluation can also be reversed by first evaluating g and then f which is what 122 | // function 'j' does: 123 | j(2) shouldBe 36 124 | } 125 | 126 | // 127 | // Methods as functions, 'ETA Expansion' (automatic coercion) 128 | // 129 | 130 | "Methods" should "be expanded to functions" in { 131 | // methods are not functions, they are methods, but they look so much 132 | // like functions! Can they be used like functions as well? 133 | 134 | // two methods 'sqr' and 'add' 135 | def sqr(x: Int) = x * x 136 | def add(x: Int) = x + x + x 137 | 138 | // when we wish to use these methods, in eg. a higher order function, then 139 | // we can use them like the methods are objects of type Function1[Int, Int], 140 | // which is clearly not the case as they are methods! But why does the 141 | // compiler not complain? 142 | 143 | def squareThenAdd(x: Int, f: Int ⇒ Int, g: Int ⇒ Int): Int = g(f(x)) 144 | squareThenAdd(2, sqr, add) shouldBe 12 145 | } 146 | 147 | it should "be expanded to functions using the Bridge Pattern" in { 148 | def add(x: Int) = x + x + x 149 | def sqr(x: Int) = x * x 150 | 151 | // squareThenAdd only accepts objects of type Function1[Int, Int], so 152 | // what is going on is that the methods are automatically coerced by the 153 | // compiler to the type Function1[Int, Int]. To show how this process works, 154 | // let's manually transform the method to a function: 155 | 156 | def squareThenAdd(x: Int, f: Int ⇒ Int, g: Int ⇒ Int): Int = g(f(x)) 157 | 158 | // create a new object of type Function[Int, Int] and 159 | // call the sqr method 160 | val f: Int ⇒ Int = new Function1[Int, Int] { 161 | override def apply(x: Int): Int = sqr(x) 162 | } 163 | 164 | // create a new object of type Function[Int, Int] and 165 | // call the add method 166 | val g: Int ⇒ Int = new Function1[Int, Int] { 167 | override def apply(x: Int): Int = add(x) 168 | } 169 | 170 | // the squareThenAdd method can be called with the two functions. We now 171 | // have manually done what the compiler does for us automatically, so 172 | // please don't do this in your projects! This process is called 'ETA Expansion'. 173 | // 174 | // There is more to ETA expansion than this, but now the story is 175 | // kind-of complete for basic Functions anyway. 176 | squareThenAdd(2, f, g) shouldBe 12 177 | } 178 | 179 | // 180 | // The functions above are always defined, which means they always give an answer, 181 | // but there are functions, that can also be applied with a value, but only evaluate 182 | // succesfully for certain inputs, let's look at some. 183 | // 184 | 185 | "A function" should "can be true for certain inputs" in { 186 | // the literal String => String is a Function1[String, String] 187 | // this type can be applied, because it is a Function, 188 | // but, when the function is not defined, scala thows a match error. 189 | // for example, the function only has an answer for the input "ping", in 190 | // which it will return a "pong", but for all other inputs, it will throw a 191 | // 'Match Error' 192 | val f: String ⇒ String = { case "ping" ⇒ "pong" } 193 | f("ping") shouldBe "pong" 194 | intercept[MatchError] { 195 | // evaluate the function by applying 'foo' to it 196 | f("foo") 197 | } 198 | } 199 | 200 | // functions that throw 'MatchErrors' are a pain, wouldn't it be nice to first check 201 | // whether or not a certain value can be applied to the function? Let's introduce the 202 | // PartialFunction type, which is a subtype of Function! 203 | 204 | "A partial function" should "be applied" in { 205 | // Note, there is no partial function literal in Scala. Scala has only 206 | // the literal '=>' which is shorthand for Function. 207 | // A partial function has the isDefinedAt method 208 | // so it can be checked 209 | val f: PartialFunction[String, String] = { case "ping" ⇒ "pong" } 210 | // we can check whether the PartialFunction is defined for a given input 211 | // which is a big plus! 212 | f.isDefinedAt("ping") shouldBe true 213 | f.isDefinedAt("foo") shouldBe false 214 | // it still throws a match error when applied though... 215 | intercept[MatchError] { 216 | // evaluate the function by applying 'foo' to it 217 | f("foo") 218 | } 219 | } 220 | 221 | // 222 | // In Scala, collections are also functions, and therefor they can be evaluated: 223 | // 224 | 225 | "A map" should "be a function" in { 226 | // a map is a function, thus is should be able to be applied 227 | val map = Map("1" -> "Foo", "2" -> "Bar") 228 | // apply '1' and '2' to the function map (strange concept coming from Java) 229 | map("1") shouldBe "Foo" 230 | map("2") shouldBe "Bar" 231 | intercept[NoSuchElementException] { 232 | map("3") shouldBe "Bar" 233 | } 234 | } 235 | 236 | "A seq" should "be a function" in { 237 | // a seq is also a function, thus .. oh well 238 | val xs = Seq(1, 2) 239 | xs(0) shouldBe 1 240 | xs(1) shouldBe 2 241 | intercept[IndexOutOfBoundsException] { 242 | xs(2) shouldBe 3 243 | } 244 | } 245 | 246 | "More complex partial function" should "be applied" in { 247 | // 'f' is a PartialFunction that can be applied with 'List[Int]' and 248 | // returns a String. 249 | val f: PartialFunction[List[Int], String] = { 250 | case Nil ⇒ "empty" 251 | case first :: second :: tail ⇒ s"$first::$second::$tail" 252 | } 253 | 254 | // As stated before, the PartialFunction can be applied with a List[Int], lets do so 255 | f(Nil) shouldBe "empty" 256 | f(List(1, 2, 3)) shouldBe "1::2::List(3)" 257 | f(List(1, 2, 3, 4)) shouldBe "1::2::List(3, 4)" 258 | f.isDefinedAt(Nil) shouldBe true 259 | f.isDefinedAt(List(1)) shouldBe false 260 | f.isDefinedAt(List(1, 2)) shouldBe true 261 | } 262 | 263 | "Nested matching" should "be applied" in { 264 | // 'f' is a PartialFunction that can be applied with 'List[Int]' and 265 | // returns a String. Note the !!nested PartialFunction!!, which is only defined 266 | // when the tail is 'Nil', but when the tail is eg. a List(1, 2, 3) the nested 267 | // PartialFunction is not defined 268 | val f: PartialFunction[List[Int], String] = { 269 | case Nil ⇒ "empty" 270 | case head :: tail ⇒ tail match { // nested PartialFunction match is not exhaustive, so it will fail with List(_) 271 | case Nil ⇒ "empty" 272 | } 273 | } 274 | 275 | // let's apply this partial function 276 | f.isDefinedAt(List(1, 2, 3)) shouldBe true 277 | // but when actually applied... 278 | intercept[MatchError] { 279 | f(List(1, 2, 3)) 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/FunctionalRandomGeneratorTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.{ Random, TestSpec } 20 | import org.scalatest.exceptions.TestFailedException 21 | import org.scalatest.prop.GeneratorDrivenPropertyChecks 22 | 23 | class FunctionalRandomGeneratorTest extends TestSpec with GeneratorDrivenPropertyChecks { 24 | 25 | /** 26 | * Simple generators, only give one result each time 27 | */ 28 | object Simple { 29 | trait Generator[+T] { 30 | def generate: T 31 | } 32 | 33 | def integers = new Generator[Int] { 34 | override def generate: Int = Random().nextInt() 35 | } 36 | 37 | def booleans = new Generator[Boolean] { 38 | override def generate: Boolean = integers.generate > 0 39 | } 40 | 41 | def pairs = new Generator[(Int, Int)] { 42 | override def generate: (Int, Int) = (integers.generate, integers.generate) 43 | } 44 | } 45 | 46 | "Random" should "create random numbers" in { 47 | Random().nextInt() shouldBe a[Integer] 48 | } 49 | 50 | "SimpleGenerator" should "generate a random number" in { 51 | Simple.integers.generate shouldBe a[Integer] 52 | } 53 | 54 | it should "generate a boolean" in { 55 | Simple.booleans.generate shouldBe a[java.lang.Boolean] 56 | } 57 | 58 | it should "generate a pair of integer" in { 59 | Simple.pairs.generate mustBe { 60 | case (_: Int, _: Int) ⇒ 61 | } 62 | } 63 | 64 | /** 65 | * A bit more advanced generator, has the map and flatMap methods 66 | */ 67 | object Advanced { 68 | trait Generator[+T] { 69 | self ⇒ 70 | 71 | def generate: T 72 | 73 | def map[S](f: T ⇒ S): Generator[S] = new Generator[S] { 74 | def generate: S = f(self.generate) 75 | } 76 | 77 | def flatMap[S](f: T ⇒ Generator[S]): Generator[S] = new Generator[S] { 78 | override def generate: S = f(self.generate).generate 79 | } 80 | } 81 | 82 | /** 83 | * Generic Generators 84 | */ 85 | def single[T](x: T) = new Generator[T] { 86 | override def generate: T = x 87 | } 88 | 89 | def choose(lo: Int, hi: Int): Generator[Int] = 90 | for (x ← integers) yield lo + x % (hi - lo) 91 | 92 | // the T* is the varargs syntax you know of Java 93 | def oneOf[T](xs: T*): Generator[T] = 94 | for (idx ← choose(0, xs.length)) yield xs(idx) 95 | 96 | def integers: Generator[Int] = new Generator[Int] { 97 | override def generate: Int = Random().nextInt() 98 | } 99 | 100 | def booleans: Generator[Boolean] = new Generator[Boolean] { 101 | override def generate: Boolean = integers.generate > 0 102 | } 103 | 104 | def pairs[T, U](t: Generator[T], u: Generator[U]) = new Generator[(T, U)] { 105 | override def generate: (T, U) = (t.generate, u.generate) 106 | } 107 | 108 | def lists: Generator[List[Int]] = for { 109 | isEmpty ← booleans 110 | list ← if (isEmpty) emptyList else nonEmptyList 111 | } yield list 112 | 113 | def emptyList: Generator[List[Int]] = single(List.empty[Int]) 114 | 115 | def nonEmptyList: Generator[List[Int]] = for { 116 | head ← integers 117 | tail ← lists 118 | } yield head :: tail 119 | 120 | def leafs: Generator[Leaf] = for { 121 | x ← integers 122 | } yield Leaf(x) 123 | 124 | def nodes: Generator[Node] = for { 125 | l ← trees 126 | r ← trees 127 | } yield Node(l, r) 128 | 129 | // it generates either a leaf or a node 130 | def trees: Generator[Tree] = for { 131 | isLeaf ← booleans 132 | tree ← if (isLeaf) leafs else nodes 133 | } yield tree 134 | } 135 | 136 | "Advanced" should "generate a random number" in { 137 | Advanced.integers.generate shouldBe a[Integer] 138 | } 139 | 140 | it should "generate a boolean" in { 141 | Advanced.booleans.generate shouldBe a[java.lang.Boolean] 142 | } 143 | 144 | it should "generate a pair of integer" in { 145 | Advanced.pairs(Advanced.integers, Advanced.integers).generate mustBe { 146 | case (_: Int, _: Int) ⇒ 147 | } 148 | } 149 | 150 | it should "generate a list" in { 151 | Advanced.lists.generate mustBe { 152 | case _: List[Int] ⇒ 153 | } 154 | } 155 | 156 | // definition of a binary tree, a tree can be either a leaf or a node 157 | // but when its a node, then it contains to its left side a tree and 158 | // to its right a tree, which either can be either... etc 159 | trait Tree 160 | case class Node(left: Tree, right: Tree) extends Tree 161 | case class Leaf(x: Int) extends Tree 162 | 163 | it should "generate trees" in { 164 | Advanced.trees.generate shouldBe a[Tree] 165 | } 166 | 167 | def test[T](g: Advanced.Generator[T], numTimes: Int = 100)(test: T ⇒ Boolean): Unit = { 168 | for (i ← 0 until numTimes) { 169 | val value: T = g.generate 170 | assert(test(value), s"test failed for $value") 171 | } 172 | println(s"passed $numTimes tests") 173 | } 174 | 175 | "testing list adding two list must always be greater" should "always fail" in { 176 | intercept[TestFailedException] { 177 | test(Advanced.pairs(Advanced.lists, Advanced.lists)) { 178 | case (xs, xy) ⇒ (xs ++ xy).length > xs.length 179 | } 180 | } 181 | } 182 | 183 | it should "aways fail using forAll" in { 184 | intercept[TestFailedException] { 185 | forAll { (l1: List[Int], l2: List[Int]) ⇒ 186 | l1.size + l2.size should not be (l1 ++ l2).size 187 | } 188 | } 189 | } 190 | 191 | it should "always succeed forAll" in { 192 | forAll { (l1: List[Int], l2: List[Int]) ⇒ 193 | l1.size + l2.size shouldBe (l1 ++ l2).size 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/JsonTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.TestSpec 20 | 21 | class JsonTest extends TestSpec { 22 | 23 | abstract class JSON 24 | case class JSeq(elems: List[JSON]) extends JSON 25 | case class JObj(bindings: Map[String, JSON]) extends JSON 26 | case class JNum(num: Double) extends JSON 27 | case class JStr(str: String) extends JSON 28 | case class JBool(b: Boolean) extends JSON 29 | case object JNull extends JSON 30 | 31 | val data = JObj(Map( 32 | "firstName" -> JStr("John"), 33 | "lastName" -> JStr("Smith"), 34 | "address" -> JObj(Map( 35 | "street" -> JStr("21 2nd Street"), 36 | "state" -> JStr("NY"), 37 | "postalCode" -> JNum(10021) 38 | )), 39 | "phoneNumbers" -> JSeq(List( 40 | JObj(Map( 41 | "type" -> JStr("Home"), "number" -> JStr("212 555-1234") 42 | )), 43 | JObj(Map( 44 | "type" -> JStr("fax"), "number" -> JStr("646 555-4567") 45 | )) 46 | )) 47 | )) 48 | 49 | def show(json: JSON): String = json match { 50 | case JSeq(elems) ⇒ "[" + (elems map show mkString ",") + "]" 51 | case JObj(bindings) ⇒ 52 | val assoc = bindings map { 53 | case (key, value) ⇒ "\"" + key + "\": " + show(value) 54 | } 55 | "{" + (assoc mkString ", ") + "}" 56 | 57 | case JNum(num) ⇒ num.toString 58 | case JStr(str) ⇒ "\"" + str + "\"" 59 | case JBool(b) ⇒ b.toString 60 | case JNull ⇒ "null" 61 | } 62 | 63 | "data" should "be json" in { 64 | show(data) shouldBe """{"firstName": "John", "lastName": "Smith", "address": {"street": "21 2nd Street", "state": "NY", "postalCode": 10021.0}, "phoneNumbers": [{"type": "Home", "number": "212 555-1234"},{"type": "fax", "number": "646 555-4567"}]}""" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/MonadTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.TestSpec 20 | 21 | class MonadTest extends TestSpec { 22 | /** 23 | * Data structures with `map` and `flatMap` are common in Scala and 24 | * functional programming. There is a name for this class of 25 | * data structures together with some algebraic laws that they 26 | * `should` have. They are called `monads`. It is the name of 27 | * a functional design pattern. 28 | */ 29 | 30 | /** 31 | * A monad `M` is a parametric type `M[T]` with two operations, 32 | * `flatMap` and `unit` and has to satisfy some laws 33 | */ 34 | 35 | trait M[T] { 36 | self ⇒ 37 | def get: T 38 | def flatMap[U](f: T ⇒ M[U]): M[U] // flatMap method is also called 'bind' 39 | def map[U](f: T ⇒ U): M[U] // in scala every monad also has a 'map' function 40 | def unit(x: T): M[T] // the Monad constructor, most often placed in the companion object of the monad 41 | } 42 | 43 | /** 44 | * Monad Laws: 45 | * To qualify as a Monad, a type has to satisfy three laws: 46 | * 47 | * Remember: flatMap accepts a function f: (T => Monad[U]): Monad[U] thus 48 | * f is T => Monad[U] and returns a Monad[U]. 49 | * 50 | * Also, 'f' and 'g' are both functions from (T => Monad[U]) 51 | * And: f(x) is an application of the function, and so, the result is Monad[U] 52 | * And unit(x) is an application of the function (T => Monad[T]): Monad[T] which creates a Monad 53 | * 54 | * 1. Associativity: 55 | * m flatMap f flatMap g == m flatMap (x => f(x) flatMap g 56 | * 57 | * Translated: it must not matter whether the evaluation is first (m flatMap f) then flatMap g, 58 | * or (the right side of the '=='), first apply x to f (which gives a monad), flatMap g, and 59 | * last m flatMap the evaluated result, thus, it should not matter where the parenthesis 60 | * are placed. The result must be the same, the sequence of evaluation should not matter 61 | */ 62 | 63 | "Rule 1: Associativity: the sequence of evaluation does not matter" should "be true" in { 64 | val x = 2 65 | val f: Int ⇒ Option[Int] = (x: Int) ⇒ Option(x * 2) 66 | val g: Int ⇒ Option[Int] = (x: Int) ⇒ Option(x * 3) 67 | Option(x).flatMap(f).flatMap(g) shouldBe Option(x).flatMap(x ⇒ f(x).flatMap(g)) 68 | } 69 | 70 | /** 71 | * 2. Left unit 72 | * unit(x) flatMap f == f(x) 73 | * 74 | * Translated: When you create a Monad (apply x to unit), and then flatMap with function a (T => Monad[U]), 75 | * it should be the same as applying x to the function f: T => Monad[U], both return a Monad[U]. 76 | * When you do only unit(x) you get a Monad[T], but when you flatMap over that Monad[T] you get a Monad[U]. 77 | * Applying x to f, should also give a Monad[U]. 78 | * 79 | * For example: 80 | * assume the function we will use is: 81 | */ 82 | 83 | "Rule 2: Left Unit" should "be true" in { 84 | val x = 2 85 | val f: Int ⇒ Option[Int] = (x: Int) ⇒ Option(x * 2) 86 | (Option(x) flatMap f) shouldBe f(x) 87 | } 88 | 89 | /** 90 | * 3. Right unit 91 | * m flatMap unit == m 92 | * 93 | * When you flatMap with the Unit constructor (which is T => Monad[T]), you don't get a Monad[U], but you get 94 | * a Monad[T] which is the same as the monad 'm', which is Monad[T]. The monad constructor 'unit' is a 95 | * function T => Monad[T], which can be applied by flatMap, and gives the monad we started out with. 96 | * For example, Option(1).flatMap(Option(1)) == Option(1) 97 | * 98 | * For example: 99 | */ 100 | 101 | "Rule 3: Right Unit" should "be true" in { 102 | val x = 1 103 | Option(x).flatMap(x ⇒ Option(x)) shouldBe Option(x) 104 | } 105 | 106 | /** 107 | * Because all three rules are true, the Option is a Monad 108 | */ 109 | } 110 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week1/PatternMatchTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week1 18 | 19 | import com.test.TestSpec 20 | 21 | class PatternMatchTest extends TestSpec { 22 | 23 | // Scala has a built-in general pattern matching mechanism. It allows to match 24 | // on any sort of data with a first-match policy. 25 | // most importantly, the match 'expression' returns a value, in this case it is 26 | // a function from (Int => String) and thus returns a String 27 | def matchNumber(x: Int): String = x match { 28 | case 1 ⇒ "one" 29 | case 2 ⇒ "two" 30 | case _ ⇒ "many" 31 | } 32 | 33 | // this is a function from (Any => Any), 34 | // so it accepts Any, and returns Any 35 | def matchAny(x: Any): Any = x match { 36 | case 1 ⇒ "one" 37 | case "two" ⇒ 2 38 | case y: Int ⇒ "scala.Int" 39 | case couldBeAnyting ⇒ couldBeAnyting 40 | } 41 | 42 | // it is also possible to set guards 43 | // on the matches 44 | def bigger(x: Any): Any = x match { 45 | case i: Int if i < 0 ⇒ i - 1 46 | case i: Int ⇒ i + 1 47 | case d: Double if d < 0.0 ⇒ d - 0.1 48 | case d: Double ⇒ d + 0.1 49 | case text: String ⇒ text + "s" 50 | } 51 | 52 | "matchNumber" should "match numbers" in { 53 | matchNumber(1) shouldBe "one" 54 | matchNumber(2) shouldBe "two" 55 | matchNumber(3) shouldBe "many" 56 | } 57 | 58 | "matchAny" should "match literally anything" in { 59 | matchAny(1) shouldBe "one" 60 | matchAny("two") shouldBe 2 61 | matchAny(2) shouldBe "scala.Int" 62 | matchAny(List(1, 2, 3)) shouldBe List(1, 2, 3) 63 | } 64 | 65 | "bigger" should "match with guard" in { 66 | bigger(-1) shouldBe -2 67 | bigger(1) shouldBe 2 68 | bigger(-1.0) shouldBe -1.1 69 | bigger(1.0) shouldBe 1.1 70 | bigger("text") shouldBe "texts" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/FRPTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2 18 | 19 | import com.test.TestSpec 20 | import com.test.week2.frp.{ BankAccount, Signal } 21 | 22 | class FRPTest extends TestSpec { 23 | def consolidated(accts: List[BankAccount]): Signal[Int] = 24 | Signal(accts.map(_.balance()).sum) 25 | 26 | "FRP" should "dynamically update signals" in { 27 | val a = new BankAccount 28 | val b = new BankAccount 29 | val c = consolidated(List(a, b)) 30 | c() shouldBe 0 31 | a deposit 20 32 | c() shouldBe 20 33 | b deposit 30 34 | c() shouldBe 50 35 | val xchange = Signal(246.00) // a constant signal 36 | val inDollar = Signal(c() * xchange()) 37 | inDollar() shouldBe 12300.0 38 | b withdraw 10 39 | inDollar() shouldBe 9840.0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/FunctionsAndStateTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2 18 | 19 | import com.test.TestSpec 20 | import com.test.week2.state.BankAccount 21 | 22 | class FunctionsAndStateTest extends TestSpec { 23 | 24 | "BankAccount" should "be able to be overdrawn" in { 25 | // account is a stateful object, because the effect of the withdraw method 26 | // depends on the history of the object. The results differ based upon the 27 | // value of the balance variable. 28 | val acct = new BankAccount 29 | acct.deposit(50) 30 | acct.withdraw(20) shouldBe 30 31 | acct.withdraw(20) shouldBe 10 32 | intercept[Error] { 33 | acct.withdraw(15) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/LoopTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2 18 | 19 | import com.test.TestSpec 20 | 21 | class LoopTest extends TestSpec { 22 | 23 | "Scala" should "support the while loop" in { 24 | // please don't do this, scala has support for declarative style 25 | var i = 0 26 | while (i < 10) { 27 | i += 1 28 | } 29 | i shouldBe 10 30 | } 31 | 32 | it should "support declarative style 1" in { 33 | (1 to 10).map(_ ⇒ 1).sum shouldBe 10 34 | } 35 | 36 | "WHILE" should "be implemented using a function" in { 37 | // 'condition' is a 'pass-by-name' parameter (note the '=>'), which means that every time 38 | // 'condition' or 'command' is called, the function will be 're-evaluated' 39 | // if it was 'pass-by-value', well maybe the loop would run forever... if the condition is true 40 | def WHILE(condition: ⇒ Boolean)(command: ⇒ Unit): Unit = 41 | if (condition) { 42 | command 43 | WHILE(condition)(command) 44 | } else () 45 | 46 | var i = 0 47 | WHILE(i < 10) { 48 | i += 1 49 | } 50 | i shouldBe 10 51 | } 52 | 53 | "REPEAT" should "be implemented using a function" in { 54 | def REPEAT(command: ⇒ Unit)(condition: ⇒ Boolean): Unit = { 55 | command 56 | if (condition) () else REPEAT(command)(condition) 57 | } 58 | 59 | var i = 0 60 | REPEAT { 61 | i += 1 62 | }(i == 10) 63 | i shouldBe 10 64 | } 65 | 66 | "REPEAT UNTIL" should "be implemented using named class" in { 67 | class Repeater(command: ⇒ Unit) { 68 | def UNTIL(condition: ⇒ Boolean): Unit = { 69 | command 70 | if (condition) () else UNTIL(condition) 71 | } 72 | } 73 | 74 | def REPEAT(command: ⇒ Unit) = new Repeater(command) 75 | var i = 0 76 | REPEAT { 77 | i += 1 78 | } UNTIL (i == 10) 79 | 80 | i shouldBe 10 81 | } 82 | 83 | it should "be implemented using AnyRef type" in { 84 | def REPEAT(command: ⇒ Unit) = new AnyRef { 85 | def UNTIL(condition: ⇒ Boolean): Unit = { 86 | command 87 | if (condition) () else UNTIL(condition) 88 | } 89 | } 90 | 91 | var i = 0 92 | REPEAT { 93 | i += 1 94 | } UNTIL (i == 10) 95 | 96 | i shouldBe 10 97 | } 98 | 99 | it should "be implemented as an anonymous class" in { 100 | def REPEAT(command: ⇒ Unit) = new { 101 | def UNTIL(condition: ⇒ Boolean): Unit = { 102 | command 103 | if (condition) () else UNTIL(condition) 104 | } 105 | } 106 | 107 | var i = 0 108 | REPEAT { 109 | i += 1 110 | } UNTIL (i == 10) 111 | 112 | i shouldBe 10 113 | } 114 | 115 | it should "be implemented using Eta expansion" in { 116 | def REPEAT(command: ⇒ Unit)(condition: () ⇒ Boolean): Unit = { 117 | command 118 | if (condition()) () else REPEAT(command)(condition) 119 | } 120 | 121 | def UNTIL(condition: ⇒ Boolean): () ⇒ Boolean = () ⇒ condition 122 | 123 | // extra braces due to currying 124 | var i = 0 125 | REPEAT { 126 | i += 1 127 | }(UNTIL(i == 10)) 128 | 129 | i shouldBe 10 130 | } 131 | 132 | "Scala" should "support the do-while loop" in { 133 | // please don't do this, scala has support for declarative style 134 | var i = 0 135 | do { 136 | i += 1 137 | } while (i < 10) 138 | i shouldBe 10 139 | } 140 | 141 | it should "support declarative style 2" in { 142 | (1 to 10).map(_ ⇒ 1).sum shouldBe 10 143 | } 144 | 145 | "Scala" should "support a for loop with generators" in { 146 | // please don't do this, scala has support for declarative style 147 | var j = 0 148 | for (i ← 1 to 3) { 149 | j += i 150 | } 151 | j shouldBe 6 152 | } 153 | 154 | it should "support declarative style 3" in { 155 | (1 to 3).sum shouldBe 6 156 | } 157 | 158 | "Nested loops" should "be evaluated" in { 159 | val xs = for (i ← 1 to 3; j ← "abc") yield s"$i$j" 160 | xs shouldBe List("1a", "1b", "1c", "2a", "2b", "2c", "3a", "3b", "3c") 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/ObjectIdentityTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2 18 | 19 | import com.test.TestSpec 20 | import com.test.week2.state.BankAccount 21 | 22 | class ObjectIdentityTest extends TestSpec { 23 | 24 | // 25 | // operational equivalence 26 | // 27 | 28 | /** 29 | * The function that isolates the test to be performed 30 | * on both objects 31 | */ 32 | def f(x: BankAccount, y: BankAccount): Unit = { 33 | x.deposit(30) 34 | y.withdraw(20) 35 | } 36 | 37 | "Two object" should "be the same when their behavior is the same" in { 38 | val x = new BankAccount 39 | val y = x 40 | // the test did not fail, x and y behave the same so they 41 | // are the same 42 | f(x, y) 43 | } 44 | 45 | it should "test whether these two object are the same" in { 46 | val x = new BankAccount 47 | val y = new BankAccount 48 | // it should fail, they are not the same 49 | intercept[Error] { 50 | f(x, y) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/ObserverTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2 18 | 19 | import com.test.TestSpec 20 | import com.test.week2.observer.{ Consolidator, BankAccount } 21 | 22 | class ObserverTest extends TestSpec { 23 | 24 | /** 25 | * The Observer Pattern is widely used when views need to react to changes 26 | * in a model. 27 | * 28 | * Variants of it are also called: 29 | * - publish / subscribe 30 | * - model / view / controller 31 | */ 32 | 33 | "Consolidator" should "compute the total balance" in { 34 | val a = new BankAccount 35 | val b = new BankAccount 36 | val c = new Consolidator(List(a, b)) 37 | c.totalBalance shouldBe 0 38 | 39 | a deposit 20 40 | c.totalBalance shouldBe 20 41 | b deposit 30 42 | c.totalBalance shouldBe 50 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/StreamsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2 18 | 19 | import com.test.TestSpec 20 | 21 | class StreamsTest extends TestSpec { 22 | 23 | /** 24 | * A 'normal' collection, like a List is called a 'strict' collection. A strict collection 25 | * evaluates all elements eagerly, even when it is not needed. 26 | */ 27 | 28 | "A List" should "evaluate all entries on head" in { 29 | var numTimes = 0 30 | val x = List(1, 2, 3).map { i ⇒ 31 | numTimes += 1 32 | i * 2 33 | }.head // only get the head, so one evaluation would be enough, but ... 34 | x shouldBe 2 35 | numTimes shouldBe 3 // the collection has been evaluated 3 times 36 | } 37 | 38 | it should "still evaluate all entries on take 2" in { 39 | var numTimes = 0 40 | val x = List(1, 2, 3).map { i ⇒ 41 | numTimes += 1 42 | i * 2 43 | }.take(2) // only get the first 2 elements, so two evaluations would be enough, but ... 44 | x shouldBe List(2, 4) 45 | numTimes shouldBe 3 // the collection has been evaluated 3 times 46 | } 47 | 48 | it should "be created using '::'" in { 49 | // lists can be created using "::" 50 | val xs = 1 :: 2 :: 3 :: Nil 51 | xs.head shouldBe 1 52 | } 53 | 54 | it should "filter the list" in { 55 | var num = 0 56 | val xs = List(3, 1, 2, 1, 4) 57 | val x = xs.filter { e ⇒ 58 | num += 1 59 | e < 2 60 | }.take(2) 61 | x shouldBe List(1, 1) 62 | num shouldBe 5 // filter evaluates all elements, even if we only want 2 63 | } 64 | 65 | /** 66 | * Lazy collections (non-strict collections) are collections that will only evaluate the 67 | * necessary elements, depending on the operations that follow the collection. 68 | * You could also call them 'on-demand' collections. 69 | */ 70 | 71 | "A Stream" should "not evaluate all entries on head" in { 72 | var numTimes = 0 73 | val x = Stream(1, 2, 3).map { i ⇒ 74 | numTimes += 1 75 | i * 2 76 | }.head // only get the head, so one evaluation would be enough, 77 | x shouldBe 2 78 | numTimes shouldBe 1 // and it did only one evaluation 79 | } 80 | 81 | it should "not evaluate all entries on take 2" in { 82 | var numTimes = 0 83 | val x = Stream(1, 2, 3).map { i ⇒ 84 | numTimes += 1 85 | i * 2 86 | }.take(2) // only get the first two elements, so two evaluations would be enough, 87 | x shouldBe List(2, 4) 88 | numTimes shouldBe 2 // and it did only two evaluations 89 | } 90 | 91 | it should "be created using '#::' and filter" in { 92 | var num = 0 93 | val xs: Stream[Int] = 3 #:: 1 #:: 2 #:: 1 #:: 4 #:: Stream.empty 94 | val x = xs.filter { e ⇒ 95 | num += 1 96 | e < 2 97 | }.take(2) 98 | x shouldBe Stream(1, 1) 99 | num shouldBe 4 // it skips the first element (3), not < 2, 100 | // then takes the second (1), skips the third (2) 101 | // and takes the fourth (1), it will not evaluate (4) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/frp/BankAccount.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.frp 18 | 19 | class BankAccount { 20 | val balance = Var(0) 21 | 22 | def deposit(amount: Int): Unit = 23 | if (amount > 0) { 24 | val b = balance() 25 | balance() = b + amount 26 | } 27 | 28 | def withdraw(amount: Int): Unit = 29 | if (0 < amount && amount <= balance()) { 30 | val b = balance() 31 | balance() = b - amount 32 | } else throw new Error("insufficient funds") 33 | } 34 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/frp/Signal.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.frp 18 | 19 | import scala.util.DynamicVariable 20 | 21 | // a special signal that has no value and no implementation 22 | object NoSignal extends Signal[Nothing](???) { 23 | override def computeValue() = () 24 | } 25 | 26 | object Signal { 27 | // The DynamicVariable is a way to obtain variables 28 | // anywhere in the code (not passing through methods 29 | // arguments eg. implicitly by means of currying), 30 | // which would be much nicer. 31 | // 32 | // The DynamicVariable uses the ThreadLocal trick, which means 33 | // that DynamicVariable binds variables to a specific thread 34 | // that processes the code. 35 | // 36 | // So it's a non-intrusive way to store and pass around 37 | // context(thread)-specific information. 38 | // 39 | // The variable is global for the thread, but it is not shared between 40 | // threads. 41 | private val caller = new DynamicVariable[Signal[_]](NoSignal) 42 | /** 43 | * Creates a new Signal 44 | */ 45 | def apply[T](expr: ⇒ T): Signal[T] = new Signal(expr) 46 | } 47 | 48 | class Signal[T](expr: ⇒ T) { 49 | import Signal._ 50 | 51 | // the current value of the signal 52 | private var myValue: T = _ 53 | 54 | // the 'current' expression that defines the signal value 55 | private var myExpr: () ⇒ T = _ 56 | 57 | // a set of observers: the other signals that depend on its value 58 | private var observers: Set[Signal[_]] = Set() 59 | 60 | update(expr) 61 | 62 | protected def update(expr: ⇒ T): Unit = { 63 | myExpr = () ⇒ expr 64 | computeValue() 65 | } 66 | 67 | protected def computeValue(): Unit = { 68 | // the withValue method will be called with a newValue, 69 | // which is this Signal, and an expression 'myExpr'. 70 | // 71 | // When, within the expression 'myExpr()', the thread tries to get 72 | // the value of a signal (by calling the apply() method of a Signal, 73 | // like eg. 'c()' from the Coursera Lecture, this operation will take 74 | // place within the scope of the second argument 'myExpr()'. 75 | // 76 | // When you scroll down you will see that apply() calls 'caller.value'. 77 | // 78 | // When the thread calls 'caller.value', it will be given back this Signal 79 | // 80 | val newValue = caller.withValue(this)(myExpr()) 81 | if (myValue != newValue) { 82 | myValue = newValue 83 | val obs = observers 84 | observers = Set() 85 | obs.foreach(_.computeValue()) 86 | } 87 | } 88 | 89 | def apply(): T = { 90 | // when the value of the signal is requested, the Signal will be added to the list of 91 | // observers which depends on the value of this signal. 92 | observers += caller.value 93 | // will throw an error, remember: 'balance() = balance() + amount' ? 94 | assert(!caller.value.observers.contains(this), "cyclic signal definition") 95 | myValue 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/frp/StackableVariable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.frp 18 | 19 | /** 20 | * A stack for Signals 21 | */ 22 | class StackableVariable[T](init: T) { 23 | // a list of Signals 24 | private var values: List[T] = List(init) 25 | // the current value 26 | def value: T = values.head 27 | // put the new value on the top of the stack 28 | def withValue[R](newValue: T)(operationToPerform: ⇒ R): R = { 29 | values = newValue :: values 30 | try operationToPerform finally values = values.tail 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/frp/Var.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.frp 18 | 19 | /** 20 | * A mutable Signal 21 | */ 22 | object Var { 23 | /** 24 | * Creates a new Var 25 | */ 26 | def apply[T](expr: ⇒ T): Var[T] = new Var(expr) 27 | } 28 | 29 | class Var[T](expr: ⇒ T) extends Signal[T](expr) { 30 | /** 31 | * Updates the signal; when it does, all the observers 32 | * need to be re-evaluated 33 | */ 34 | override def update(expr: ⇒ T): Unit = super.update(expr) 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/observer/BankAccount.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.observer 18 | 19 | class BankAccount extends Publisher { 20 | private var balance = 0 21 | 22 | /** 23 | * When the state changes, notify all subscribers 24 | * Bankaccount is called a publisher because it 25 | * publishes the change 'event', which is implicit 26 | * in this case. The fact that it calls the publish() 27 | * method is an event in itself 28 | * @param amount 29 | */ 30 | def deposit(amount: Int): Unit = { 31 | balance = if (amount > 0) balance + amount else balance 32 | publish() 33 | } 34 | 35 | /** 36 | * Also notify subscribers 37 | * @param amount 38 | */ 39 | def withdraw(amount: Int): Unit = 40 | if (0 < amount && amount <= balance) { 41 | balance -= amount 42 | publish() 43 | } else { 44 | throw new Error("insufficient funds") 45 | } 46 | 47 | def currentBalance = balance 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/observer/Consolidator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.observer 18 | 19 | /** 20 | * Observes a list of bankaccounts and is always 21 | * up to date with the total balance of all the 22 | * bankaccounts (sum of all the balances) 23 | */ 24 | class Consolidator(observed: List[BankAccount]) extends Subscriber { 25 | // subscribe to each of the bankaccounts 26 | // The 'trick' is that the list of bankaccounts *are* the publishers, 27 | // so on each of the bankaccount, consolidator subscribes itself (this) 28 | observed.foreach(bankaccount ⇒ bankaccount.subscribe(this)) 29 | 30 | private var total: Int = _ 31 | compute() 32 | 33 | private def compute() = 34 | total = observed.map(_.currentBalance).sum 35 | 36 | /** 37 | * When the publisher calls the 'handler' method, the consolidator will 38 | * recompute the total balance 39 | * @param pub 40 | */ 41 | def handler(pub: Publisher) = compute() 42 | 43 | def totalBalance = total 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/observer/Publisher.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.observer 18 | 19 | trait Publisher { 20 | private var subscribers: Set[Subscriber] = Set() 21 | 22 | /** 23 | * Add a subscriber to the set 24 | * @param subscriber 25 | */ 26 | def subscribe(subscriber: Subscriber): Unit = 27 | subscribers += subscriber 28 | 29 | /** 30 | * Remove a subscriber from the set 31 | * @param subscriber 32 | */ 33 | def unsubscribe(subscriber: Subscriber): Unit = 34 | subscribers -= subscriber 35 | 36 | /** 37 | * Publish the change to all subscribers 38 | */ 39 | def publish(): Unit = 40 | subscribers.foreach(_.handler(this)) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/observer/Subscriber.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.observer 18 | 19 | trait Subscriber { 20 | def handler(pub: Publisher): Unit 21 | } 22 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week2/state/BankAccount.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week2.state 18 | 19 | class BankAccount { 20 | private var balance = 0 21 | def deposit(amount: Int): Unit = { 22 | balance = if (amount > 0) balance + amount else balance 23 | } 24 | 25 | def withdraw(amount: Int): Int = 26 | if (0 < amount && amount <= balance) { 27 | balance -= amount 28 | balance 29 | } else { 30 | throw new Error("insufficient funds") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week3/AdventureGameOneTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week3 18 | 19 | import com.test.TestSpec 20 | 21 | case class Coin(value: Int = 1) 22 | case class Treasure(value: Long = Long.MaxValue) 23 | 24 | class GameOverException(message: String) extends RuntimeException(message) 25 | 26 | class AdventureGameOneTest extends TestSpec { 27 | 28 | class SaveAdventure { 29 | def collectCoins(): List[Coin] = (1 to 10).toList.map(Coin(_)) 30 | def buyTreasure(coins: List[Coin]): Treasure = Treasure() 31 | } 32 | 33 | class DangerousAdventure(eatenByMonster: Boolean) { 34 | val treasureCost = Integer.MAX_VALUE 35 | def collectCoins(): List[Coin] = 36 | if (eatenByMonster) 37 | throw new GameOverException("Game over man, game over!") 38 | else (1 to 10).toList.map(Coin) 39 | 40 | def buyTreasure(coins: List[Coin]): Treasure = 41 | if (coins.map(_.value).sum < treasureCost) 42 | throw new GameOverException("Nice try!") 43 | else Treasure() 44 | } 45 | 46 | // Note, all calls are blocking T => S 47 | 48 | // 49 | // Exceptions as an effect LOL :) 50 | // 51 | 52 | "SimpleAdventure" should "buy treasure" in { 53 | val adventure = new SaveAdventure() 54 | val coins = adventure.collectCoins() 55 | adventure.buyTreasure(coins) shouldBe Treasure() 56 | } 57 | 58 | "DangerousAdventure" should "hero got eaten" in { 59 | val adventure = new DangerousAdventure(eatenByMonster = true) 60 | intercept[GameOverException] { 61 | adventure.collectCoins() 62 | } 63 | } 64 | 65 | it should "hero has magic armor" in { 66 | val adventure = new DangerousAdventure(eatenByMonster = false) 67 | val coins = adventure.collectCoins() 68 | intercept[GameOverException] { 69 | adventure.buyTreasure(coins) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week3/AdventureGameTwoTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week3 18 | 19 | import java.util.NoSuchElementException 20 | 21 | import com.test.TestSpec 22 | 23 | import scala.util.{ Failure, Success, Try } 24 | 25 | class AdventureGameTwoTest extends TestSpec { 26 | 27 | class Adventure(eatenByMonster: Boolean) { 28 | val treasureCost = Integer.MAX_VALUE 29 | def collectCoins(): Try[List[Coin]] = Try { 30 | if (eatenByMonster) 31 | throw new GameOverException("Game over man, game over!") 32 | else (1 to 10).toList.map(Coin) 33 | } 34 | 35 | def buyTreasure(coins: List[Coin]): Try[Treasure] = Try { 36 | if (coins.map(_.value).sum < treasureCost) 37 | throw new GameOverException("Nice try!") 38 | else Treasure() 39 | } 40 | } 41 | 42 | "Adventure" should "hero got eaten" in { 43 | val adventure = new Adventure(eatenByMonster = true) 44 | adventure.collectCoins() should be a 'failure // hero got eaten by monster 45 | } 46 | 47 | it should "hero has magic armor" in { 48 | val adventure = new Adventure(eatenByMonster = false) 49 | adventure.collectCoins() should be a 'success // hero upgraded with magic armor 50 | } 51 | 52 | it should "buy treasure" in { 53 | val adventure = new Adventure(eatenByMonster = false) 54 | val coins = adventure.collectCoins().success.value // using ScalaTest's TryValues trait see TestSpec 55 | adventure.buyTreasure(coins) should be a 'failure // too expensive 56 | } 57 | 58 | it should "buy treasure matching, returning coins" in { 59 | val adventure = new Adventure(eatenByMonster = false) 60 | val coins: List[Coin] = adventure.collectCoins() match { 61 | case Success(listOfCoins) ⇒ listOfCoins 62 | case Failure(t) ⇒ Nil 63 | } 64 | adventure.buyTreasure(coins) should be a 'failure // too expensive 65 | } 66 | 67 | it should "buy treasure matching, returning treasure" in { 68 | val adventure = new Adventure(eatenByMonster = false) 69 | val treasure: Try[Treasure] = adventure.collectCoins() match { 70 | case Success(coins) ⇒ adventure.buyTreasure(coins) 71 | case Failure(e) ⇒ Failure(e) 72 | } 73 | treasure should be a 'failure // too expensive 74 | } 75 | 76 | it should "buy treasure using composition" in { 77 | val adventure = new Adventure(eatenByMonster = false) 78 | val treasure: Try[Treasure] = adventure.collectCoins().flatMap(coins ⇒ adventure.buyTreasure(coins)) 79 | treasure should be a 'failure // too expensive 80 | } 81 | 82 | it should "buy treasure using composition, shorter version" in { 83 | val adventure = new Adventure(eatenByMonster = false) 84 | val treasure: Try[Treasure] = adventure.collectCoins().flatMap(adventure.buyTreasure) 85 | treasure should be a 'failure // too expensive 86 | } 87 | 88 | it should "buy treasure using for comprehension" in { 89 | val adventure = new Adventure(eatenByMonster = false) 90 | val treasure: Try[Treasure] = for { 91 | coins ← adventure.collectCoins() 92 | treasure ← adventure.buyTreasure(coins) 93 | } yield treasure 94 | treasure should be a 'failure // too expensive 95 | } 96 | 97 | it should "hero retries when failed" in { 98 | val adventure = new Adventure(eatenByMonster = true) 99 | val coins: Try[List[Coin]] = adventure.collectCoins() recoverWith { 100 | case t: Throwable ⇒ 101 | // hero failed, but he tries again 102 | val newadventure = new Adventure(eatenByMonster = false) 103 | newadventure.collectCoins() 104 | } 105 | val treasure: Try[Treasure] = coins.flatMap(adventure.buyTreasure) 106 | treasure should be a 'failure // too expensive 107 | } 108 | 109 | it should "hero is Indiana Jones, he always gets treasure" in { 110 | val adventure = new Adventure(eatenByMonster = true) 111 | val treasure: Try[Treasure] = 112 | adventure.collectCoins() 113 | .recoverWith { 114 | case t: Throwable ⇒ 115 | new Adventure(eatenByMonster = false).collectCoins() 116 | } 117 | .flatMap { coins ⇒ 118 | adventure.buyTreasure(coins) 119 | .recover { 120 | case t: Throwable ⇒ 121 | Treasure() // Indiana Jones always gets treasure! 122 | } 123 | } 124 | 125 | treasure should be a 'success 126 | } 127 | 128 | it should "support other higher order functions" in { 129 | val x = Try(1) 130 | x should be a 'success 131 | x should not be 'failure 132 | x shouldBe Success(1) 133 | x should not be Failure 134 | x.get shouldBe 1 135 | x.getOrElse(0) shouldBe 1 136 | x.filter(_ == 1) shouldBe Success(1) 137 | x.filter(_ == 0) should be a 'failure 138 | x.foreach { x ⇒ assert(x == 1) } 139 | x.isFailure shouldBe false 140 | x.isSuccess shouldBe true 141 | x.toOption shouldBe Some(1) 142 | x.recoverWith { case _ ⇒ Try(2) } recover { case _ ⇒ 3 } shouldBe Success(1) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week3/AsyncAwaitTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week3 18 | 19 | import com.test.TestSpec 20 | 21 | import scala.async.Async._ 22 | import scala.concurrent.Future 23 | import scala.util.{ Failure, Try } 24 | 25 | class AsyncAwaitTest extends TestSpec { 26 | 27 | // 28 | // Note: we should import: 29 | // "org.scala-lang.modules" %% "scala-async" % "0.9.2", 30 | // 31 | // please read: https://github.com/scala/async 32 | // it explains what is going on with the async..await 33 | // construct, as it is being rewritten to non-blocking code 34 | // 35 | 36 | // 37 | // I don't know if I will use it... oh well, choices.. 38 | // 39 | 40 | def slowCalcFuture: Future[Int] = Future { 41 | Thread.sleep(200) 42 | 1 43 | } 44 | 45 | /** 46 | * Note, the async..await will be rewritten by the 47 | * compiler, it returns a future, and nothing is blocked 48 | * on.. but, we can 'reason' about it in a blocked way 49 | * within the async {} block 50 | * 51 | * What do 'they' say about Macro's every single time? 52 | * .. -> don't use it. 53 | */ 54 | def combined: Future[Int] = async { 55 | // execute sequentially 56 | await(slowCalcFuture) + await(slowCalcFuture) 57 | } 58 | 59 | /** 60 | * Execute in parallel 61 | */ 62 | def parallel: Future[Int] = async { 63 | val f1 = slowCalcFuture 64 | val f2 = slowCalcFuture 65 | await(f1) + await(f2) 66 | } 67 | 68 | "AsyncAwait" should "evaluate combined" in { 69 | combined.toTry should be a 'success 70 | combined.futureValue shouldBe 2 71 | } 72 | 73 | it should "evaluate parallel" in { 74 | parallel.toTry should be a 'success 75 | parallel.futureValue shouldBe 2 76 | } 77 | 78 | // 79 | // HipsterFuture 80 | // 81 | 82 | implicit class HipsterFuture[T](f: Future[T]) { 83 | def fallbackTo(that: ⇒ Future[T]): Future[T] = { 84 | f.recoverWith { 85 | case _ ⇒ that recoverWith { 86 | case _ ⇒ f 87 | } 88 | } 89 | } 90 | 91 | // imperative.. 92 | def retry(noTimes: Int, block: ⇒ Future[T]): Future[T] = async { 93 | var i = 0 94 | var result: Try[T] = Failure(new Exception("_")) 95 | while (result.isFailure && i < noTimes) { 96 | result = await(block) 97 | i += 1 98 | } 99 | result.get 100 | } 101 | 102 | implicit def futureOfTToFutureOfTryOfT(f: Future[T]): Future[Try[T]] = 103 | Future(f.toTry) 104 | 105 | // retries the future num times, then retries 'that' 106 | // num times (seems more interesting to me) 107 | def retryWith(noTimes: Int)(that: ⇒ Future[T]): Future[T] = { 108 | retry(noTimes, f).fallbackTo(retry(noTimes, that)) 109 | } 110 | 111 | def filter(p: T ⇒ Boolean): Future[T] = async { 112 | val x = await(f) 113 | if (!p(x)) throw new NoSuchElementException else x 114 | } 115 | } 116 | 117 | val socket = Socket() 118 | import socket._ 119 | 120 | "HipsterFuture" should "support the retry combinator" in { 121 | val packet = readFromMemory.futureValue 122 | val result: Future[Array[Byte]] = 123 | sendToEurope(packet, failed = true) 124 | .retryWith(3)(sendToUsa(packet, failed = false)) 125 | 126 | result.toTry should be a 'success 127 | } 128 | 129 | it should "support the new filter combinator" in { 130 | Future(1).filter(_ == 1).toTry should be a 'success 131 | Future(1).filter(_ == 0).toTry should be a 'failure 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week3/ComposingFuturesTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week3 18 | 19 | import com.test.TestSpec 20 | 21 | import scala.concurrent.Future 22 | 23 | class ComposingFuturesTest extends TestSpec { 24 | 25 | // 26 | // Once you introduce Futures, you are in FutureLand.. LoL! 27 | // 28 | 29 | implicit class HipsterFuture[T](f: Future[T]) { 30 | def fallbackTo(that: ⇒ Future[T]): Future[T] = { 31 | f.recoverWith { 32 | case _ ⇒ that recoverWith { 33 | case _ ⇒ f 34 | } 35 | } 36 | } 37 | 38 | // executes the block number of times 39 | def retry(noTimes: Int)(block: ⇒ Future[T]): Future[T] = { 40 | if (noTimes == 0) 41 | Future.failed(new Exception("Sorry")) 42 | else 43 | block.fallbackTo(retry(noTimes - 1)(block)) 44 | } 45 | 46 | // retries the future num times, then retries 'that' 47 | // num times (seems more interesting to me) 48 | def retryWith(noTimes: Int)(that: ⇒ Future[T]): Future[T] = { 49 | retry(noTimes)(f).fallbackTo(retry(noTimes)(that)) 50 | } 51 | } 52 | 53 | val socket = Socket() 54 | import socket._ 55 | 56 | "HipsterFuture" should "support the retry combinator" in { 57 | val packet = readFromMemory.futureValue 58 | val result: Future[Array[Byte]] = 59 | sendToEurope(packet, failed = true) 60 | .retryWith(3)(sendToUsa(packet, failed = false)) 61 | 62 | result.toTry should be a 'success 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week3/FuturesTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week3 18 | 19 | import com.test.TestSpec 20 | import scala.concurrent.duration._ 21 | import scala.concurrent._ 22 | import scala.util.{ Failure, Success } 23 | 24 | class FuturesTest extends TestSpec { 25 | 26 | /** 27 | * Futures provide a nice way to reason about performing many operations in parallel– in an efficient and non-blocking way. 28 | * The idea is simple, a Future is a sort of a placeholder object that you can create for a result that does not yet exist. 29 | * Generally, the result of the Future is computed concurrently and can be later collected. 30 | * 31 | * Composing concurrent tasks in this way tends to result in faster, asynchronous, non-blocking parallel code. 32 | * 33 | * By default, futures and promises are non-blocking, making use of callbacks instead of typical blocking operations. 34 | * 35 | * To simplify the use of callbacks both syntactically and conceptually, Scala provides combinators such as flatMap, 36 | * foreach, and filter used to compose futures in a non-blocking way. Blocking is still possible - for cases where it 37 | * is absolutely necessary, futures can be blocked on (although this is discouraged). 38 | */ 39 | 40 | /** 41 | * To use a future, we need to import an ExecutionContext, that provides the thread that will execute the Future. 42 | * Scala provides one out of the box, just add the following two imports to your code: 43 | */ 44 | 45 | //import ExecutionContext.Implicits.global // if you don't have an Actor system in scope.. then import this 46 | 47 | /** 48 | * Because we inherit an Actor system from the TestSpec, we will use its executionContext to schedule the Future(s) on. 49 | * When creating a Future, you actually use the Future's companion object named 'Future', and you will apply the 50 | * code block, the statements between the '{' and '}'. When you look at the source code 51 | * here: Future.apply() 52 | * you'll see that the compiler will 'inject' an execution context. The 'dependency injection' mechanism for Scala 53 | * are implicits. Quick disclaimer, this is not exactly true, but it is enough to know for now. So just place the 54 | * execution context in implicit scope, and the compiler will do the rest. 55 | * 56 | * It is inherited from TestSpec 57 | */ 58 | // implicit val executionContext: ExecutionContext = system.dispatcher 59 | 60 | /** 61 | * A Future is an object holding a value which may become available at some point. This value is usually the result of 62 | * some other computation: 63 | * 64 | * If the computation has not yet completed, we say that the Future is not completed. 65 | * If the computation has completed with a value or with an exception, we say that the Future is completed. 66 | * 67 | * Completion can take one of two forms: 68 | * 69 | * 1. When a Future is completed with a value, we say that the future was successfully completed with that value. 70 | * 2. When a Future is completed with an exception thrown by the computation, we say that the Future was failed with that exception. 71 | */ 72 | 73 | /** 74 | * A Future has an important property that it may only be assigned once. Once a Future object is given a value or an 75 | * exception, it becomes in effect immutable– it can never be overwritten. 76 | * 77 | * The simplest way to create a future object is to use the Future companion object and use the apply method and pass 78 | * a body to the Future, which starts an asynchronous computation: 79 | * 80 | * def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T] 81 | */ 82 | 83 | "FutureConcept" should "complete with value 2, sometime in the future" in { 84 | val future: Future[Int] = Future { 1 }.map(_ * 2) 85 | future.isReadyWithin(1.minute) shouldBe true 86 | future.futureValue shouldBe 2 87 | } 88 | 89 | /** 90 | * We can block the current thread and wait for the future to complete to 91 | * get the result: 92 | */ 93 | 94 | "Future waiting" should "block the thread until the result is available" in { 95 | val future: Future[Int] = Future(1).map(_ * 2) 96 | val result: Int = Await.result(future, 1.minute) 97 | result shouldBe 2 98 | } 99 | 100 | /** 101 | * A future is the asynchronous version of Try[T], so the result type of a future is Success[T] or Failure[T], where 102 | * the failure contains the throwable. To get to the result of the future, we can block our thread and wait 103 | * for the result like so: 104 | */ 105 | 106 | "Future callback" should "block the thread, until the thread is ready and test using callback" in { 107 | val future: Future[Int] = Future(1).map(_ * 2) 108 | // the onComplete is the callback 109 | future.onComplete { 110 | case Success(n) ⇒ n shouldBe 2 111 | case Failure(t) ⇒ fail(t) 112 | } 113 | Await.ready(future, 1.minute) 114 | // block the test thread to let the future evaluate 115 | Thread.sleep(100) 116 | } 117 | 118 | /** 119 | * The computation of a future can fail: 120 | */ 121 | 122 | "Failed future" should "complete with throwable" in { 123 | val future: Future[Int] = Future { 2 / 0 } 124 | future.onComplete { 125 | case Failure(t: ArithmeticException) ⇒ 126 | case Failure(t) ⇒ fail(t) 127 | case Success(n) ⇒ fail("Should not complete successfully") 128 | } 129 | Await.ready(future, 1.minute) 130 | Thread.sleep(100) 131 | } 132 | 133 | "Failed future" should "recover with a default value" in { 134 | val future: Future[Int] = Future { 2 / 0 } recover { case t: Throwable ⇒ 0 } 135 | future.onComplete { 136 | case Success(n) ⇒ n shouldBe 0 137 | case Failure(t) ⇒ fail(t) 138 | } 139 | Await.ready(future, 1.minute) 140 | Thread.sleep(100) 141 | } 142 | 143 | "Future composition" should "map the result of the future with a new calculation" in { 144 | val future: Future[Int] = Future { 2 }.map(_ * 2) recover { case t: Throwable ⇒ 0 } 145 | future.futureValue shouldBe 4 146 | } 147 | 148 | "Future composition can fail" should "should still recover with default value" in { 149 | val future: Future[Int] = Future { 2 }.map(_ / 0) recover { case t: Throwable ⇒ 0 } 150 | future.futureValue shouldBe 0 151 | } 152 | 153 | "Multiple futures" should "be able to be composed" in { 154 | val f1: Future[Int] = Future { 2 } 155 | val f2: Future[Int] = Future { 4 } 156 | 157 | // to return a Future[Int], we must destroy (flatten) the container 'f1', so 158 | // we have the value 2, make the calculation and return a Future 159 | val f3: Future[Int] = f1 flatMap { n ⇒ f2.map { _ * n } } 160 | f3.futureValue shouldBe 8 161 | } 162 | 163 | "Future for comprehension" should "result in the value 8" in { 164 | val f1: Future[Int] = Future { 2 } 165 | val f2: Future[Int] = Future { 4 } 166 | 167 | val f3: Future[Int] = for { 168 | n1 ← f1 // take the value n1 out of container f1 169 | n2 ← f2 // take the value n2 out of container f2 170 | } yield n1 * n2 171 | // do the processing and return container f3 172 | 173 | f3.futureValue shouldBe 8 174 | } 175 | 176 | "Future with side effects" should "leave the result alone" in { 177 | val f1 = Future { 1 } 178 | .andThen { case Success(n: Int) ⇒ n * 2 } 179 | .andThen { case Success(n: Int) ⇒ n * 2 } 180 | .andThen { case Success(n: Int) ⇒ n * 2 } 181 | 182 | f1.futureValue shouldBe 1 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week3/LatencyAsAnEffectTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week3 18 | 19 | import com.test.TestSpec 20 | import scala.concurrent.Future 21 | 22 | class LatencyAsAnEffectTest extends TestSpec { 23 | 24 | // 25 | // Latency as an effect, aka. Future 26 | // It is a Monad that capture the effect that 27 | // computations take time, and can fail, so it 28 | // captures both latency and failure. 29 | // 30 | 31 | // 32 | // combinators; another word word the higher-order-functions 33 | // in where you combine multiple higher order functions 34 | // to get new behavior 35 | // 36 | 37 | // 38 | // callbacks are sometimes called continuations. 39 | // most often functions from T => Unit (success-callback) 40 | // or Throwable => Unit (failure-callback) 41 | // 42 | 43 | val socket = Socket() 44 | import socket._ 45 | 46 | // 47 | // Note: futureValue and toTry are helper methods that are only available in 48 | // test suites. Please *do not* use these constructs in your production code. 49 | // They basically block the test execution to allow the test to assert certain 50 | // results. Please do not use in production, they do more harm than good! 51 | // 52 | // There is one rule: Once you're asynchronous, you should never-ever-block! 53 | // 54 | 55 | "Socket" should "read from memory and send to Europe" in { 56 | val fromEurope: Future[Array[Byte]] = readFromMemory.flatMap(sendToEurope(_)) 57 | fromEurope.toTry should be a 'success 58 | fromEurope.futureValue.getString shouldBe "fromMemory->fromEurope" 59 | } 60 | 61 | it should "still fail when an error occurs on the network" in { 62 | val fromEurope = readFromMemory.flatMap(sendToEurope(_, failed = true)) 63 | fromEurope.toTry should be a 'failure 64 | fromEurope.toTry.failure.exception.getMessage should include("fromEurope") 65 | } 66 | 67 | it should "let's send the packet twice, to europe and usa, and zip the result" in { 68 | val fromEurope = readFromMemory.flatMap(sendToEurope(_, failed = true)) 69 | val fromUsa = readFromMemory.flatMap(sendToUsa(_, failed = false)) 70 | // you get a future of a pair of an array of bytes (that's what zip does) 71 | val response: Future[(Array[Byte], Array[Byte])] = fromEurope.zip(fromUsa) 72 | response.toTry should be a 'failure // because one of the futures failed, bummer 73 | response.toTry.failure.exception.getMessage should include("fromEurope") 74 | } 75 | 76 | it should "first send to europe, when it fails, only then send to usa" in { 77 | // we now need a handle to the packet we want to send to be reused for the send to Usa case 78 | val packet = readFromMemory.futureValue 79 | val result: Future[Array[Byte]] = 80 | sendToEurope(packet, failed = true) 81 | .recoverWith { 82 | case t: Throwable ⇒ 83 | sendToUsa(packet, failed = false) 84 | } 85 | 86 | result.toTry should be a 'success 87 | result.futureValue.getString shouldBe "fromMemory->fromUsa" 88 | } 89 | 90 | // 91 | // Now for some 'hipster functional programming'... 92 | // 93 | 94 | // 95 | // To be a true hipster, we must know about 'implicit conversions' 96 | // To add some methods to a type, we first convert that type 97 | // to another type, and then add the functionality eg to 98 | // add the 'fallbackTo' method to a future, we will create 99 | // an implicit class HipsterFuture (you can call it anything you 100 | // wish) and then add the method eg: 101 | // 102 | 103 | implicit class HipsterFuture[T](f: Future[T]) { 104 | 105 | // When the future 'f' fails, it will execute future 'that', 106 | // and it 'that' fails, it will return the failed future 'f' 107 | def fallbackTo(that: ⇒ Future[T]): Future[T] = { 108 | f.recoverWith { 109 | case _ ⇒ that recoverWith { 110 | case _ ⇒ f 111 | } 112 | } 113 | } 114 | } 115 | 116 | "HipsterFuture" should "support the fallbackTo combinator" in { 117 | val packet = readFromMemory.futureValue 118 | val response: Future[Array[Byte]] = // the normal Future will be implicitly converted to HipsterFuture 119 | sendToEurope(packet, failed = true) 120 | .fallbackTo(sendToUsa(packet, failed = true)) 121 | // the fallBackTo combinator is quite nice. It factors 122 | // away all the ugly recoverWith code 123 | response.toTry should be a 'failure 124 | response.toTry.failure.exception.getMessage should include("fromEurope") 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week3/PromiseTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week3 18 | 19 | import com.test.TestSpec 20 | 21 | import scala.async.Async._ 22 | import scala.concurrent.{ Promise, Future } 23 | import scala.util.{ Success, Failure } 24 | 25 | class PromiseTest extends TestSpec { 26 | 27 | // 28 | // Promise is a construct which are a way to create 29 | // Futures where you can set the result value of the 30 | // Future on the outside (success/failure) 31 | // 32 | 33 | // 34 | // HipsterFuture 35 | // 36 | 37 | implicit class HipsterFuture[T](f: Future[T]) { 38 | def fallbackTo(that: ⇒ Future[T]): Future[T] = { 39 | f.recoverWith { 40 | case _ ⇒ that recoverWith { 41 | case _ ⇒ f 42 | } 43 | } 44 | } 45 | 46 | // executes the block number of times 47 | def retry(noTimes: Int)(block: ⇒ Future[T]): Future[T] = { 48 | if (noTimes == 0) 49 | Future.failed(new Exception("Sorry")) 50 | else 51 | block.fallbackTo(retry(noTimes - 1)(block)) 52 | } 53 | 54 | // retries the future num times, then retries 'that' 55 | // num times (seems more interesting to me) 56 | def retryWith(noTimes: Int)(that: ⇒ Future[T]): Future[T] = { 57 | retry(noTimes)(f).fallbackTo(retry(noTimes)(that)) 58 | } 59 | 60 | // the filter using a promise 61 | def filter(p: T ⇒ Boolean): Future[T] = { 62 | val promise = Promise[T]() 63 | f.onComplete { 64 | case Failure(t) ⇒ promise.failure(t) 65 | case Success(x) ⇒ 66 | if (!p(x)) 67 | promise.failure(new NoSuchElementException) 68 | else 69 | promise.success(x) 70 | } 71 | promise.future 72 | } 73 | } 74 | 75 | def race[T](left: Future[T], right: Future[T]): Future[T] = { 76 | val promise = Promise[T]() 77 | left.onComplete(promise.tryComplete) 78 | right.onComplete(promise.tryComplete) 79 | promise.future 80 | } 81 | 82 | def zip[T, S, R](p: Future[T], q: Future[S])(f: (T, S) ⇒ R): Future[R] = { 83 | val promise = Promise[R]() 84 | p.onComplete { 85 | case Failure(t) ⇒ promise.failure(t) 86 | case Success(x) ⇒ q.onComplete { 87 | case Failure(t) ⇒ promise.failure(t) 88 | case Success(y) ⇒ promise.success(f(x, y)) 89 | } 90 | } 91 | promise.future 92 | } 93 | 94 | def zipAwait[T, S, R](p: Future[T], q: Future[S])(f: (T, S) ⇒ R): Future[R] = async { 95 | f(await(p), await(q)) 96 | } 97 | 98 | def sequence[T](fxs: List[Future[T]]): Future[List[T]] = { 99 | fxs match { 100 | case Nil ⇒ Future(Nil) 101 | case (head :: tail) ⇒ 102 | head.flatMap(x ⇒ 103 | sequence(tail) 104 | .flatMap(xs ⇒ Future(x :: xs)) 105 | ) 106 | } 107 | } 108 | 109 | "HipsterFuture" should "support the filter combinator" in { 110 | Future(1).filter(_ == 1).toTry should be a 'success 111 | Future(1).filter(_ == 0).toTry should be a 'failure 112 | } 113 | 114 | "race" should "complete the first the fastest future" in { 115 | def sleep(millis: Long) = Thread.sleep(millis) 116 | val result: Future[Int] = race(Future { sleep(250); 1 }, Future { sleep(750); 2 }) 117 | result.toTry should be a 'success 118 | result.futureValue shouldBe 1 119 | } 120 | 121 | it should "complete the second the fastest future" in { 122 | def sleep(millis: Long) = Thread.sleep(millis) 123 | val result: Future[Int] = race(Future { sleep(750); 1 }, Future { sleep(250); 2 }) 124 | result.toTry should be a 'success 125 | result.futureValue shouldBe 2 126 | } 127 | 128 | "zip" should "zip two success futures" in { 129 | val result: Future[Int] = zip(Future(2), Future(3)) { 130 | case (x, y) ⇒ x * y 131 | } 132 | result.toTry should be a 'success 133 | result.futureValue shouldBe 6 134 | } 135 | 136 | it should "fail when one future fails" in { 137 | val result: Future[Int] = zip(Future(1 / 0), Future(3)) { 138 | case (x, y) ⇒ x * y 139 | } 140 | result.toTry should be a 'failure 141 | } 142 | 143 | "zipAwait" should "zip two success futures" in { 144 | val result: Future[Int] = zipAwait(Future(4), Future(5)) { 145 | case (x, y) ⇒ x * y 146 | } 147 | result.toTry should be a 'success 148 | result.futureValue shouldBe 20 149 | } 150 | 151 | it should "fail when one future fails" in { 152 | val result: Future[Int] = zipAwait(Future(1 / 0), Future(3)) { 153 | case (x, y) ⇒ x * y 154 | } 155 | result.toTry should be a 'failure 156 | } 157 | 158 | "sequence" should "return a list of numbers" in { 159 | val xs: Future[List[Int]] = sequence(List(Future(1), Future(2), Future(3))) 160 | xs.toTry should be a 'success 161 | xs.futureValue shouldBe List(1, 2, 3) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week4/EarthquakeTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week4 18 | 19 | import com.test.TestSpec 20 | import rx.lang.scala._ 21 | 22 | import scala.concurrent.Future 23 | 24 | class EarthQuakeTest extends TestSpec { 25 | import Usgs._ 26 | 27 | "EarthQuake stream" should "be queried" in { 28 | Usgs.stream 29 | .map(q ⇒ (q.location, Magnitude(q.magnitude.toInt))) 30 | .filter({ 31 | case (loc, magnitude) ⇒ 32 | magnitude.min >= Major.min 33 | }) 34 | .take(3) 35 | .map(_._2) 36 | .toBlocking 37 | .toList should contain atLeastOneOf (Major, Great) 38 | } 39 | 40 | it should "translate geocode" in { 41 | Usgs.stream 42 | .map { quake ⇒ 43 | val country: Future[Country] = reverseGeoCode(quake.location) 44 | Observable.from(country.map(c ⇒ (quake, c))) 45 | } 46 | .flatten 47 | .filter({ 48 | case (_, Country("America")) ⇒ true 49 | case _ ⇒ false 50 | }) 51 | .take(2) 52 | .map(_._2.name) 53 | .toBlocking 54 | .toList should contain("America") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week4/IterableTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week4 18 | 19 | import com.test.TestSpec 20 | 21 | import scala.collection.immutable.TreeSet 22 | 23 | class IterableTest extends TestSpec { 24 | 25 | "List" should "be an Iterable" in { 26 | List(1, 2, 3) shouldBe an[Iterable[_]] 27 | } 28 | 29 | it should "have next elements" in { 30 | List(1, 2, 3).iterator should have('hasNext(true)) 31 | } 32 | 33 | it should "contain elements" in { 34 | List(Seq(0, 1), Seq(1, 2)) should contain inOrder (Seq(0, 1), Seq(1, 2)) 35 | } 36 | 37 | "Vector" should "be an Iterable" in { 38 | Vector(1, 2, 3) shouldBe an[Iterable[_]] 39 | } 40 | 41 | it should "have next elements" in { 42 | Vector(1, 2, 3).iterator should have('hasNext(true)) 43 | } 44 | 45 | "Set" should "be an Iterable" in { 46 | Set(1, 2, 3) shouldBe an[Iterable[_]] 47 | } 48 | 49 | it should "have next elements" in { 50 | Set(1, 2, 3).iterator should have('hasNext(true)) 51 | } 52 | 53 | "Tree" should "be an Iterable" in { 54 | TreeSet(1, 2, 3) shouldBe an[Iterable[_]] 55 | } 56 | 57 | it should "have next elements" in { 58 | TreeSet(1, 2, 3).iterator should have('hasNext(true)) 59 | } 60 | 61 | "Map" should "be an Iterable" in { 62 | Map(1 -> "1", 2 -> "2", 3 -> "3") shouldBe an[Iterable[_]] 63 | } 64 | 65 | it should "have next elements" in { 66 | Map(1 -> "1", 2 -> "2", 3 -> "3").iterator should have('hasNext(true)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week4/PromisesAndSubjectsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week4 18 | 19 | import com.test.TestSpec 20 | import rx.lang.scala.Subject 21 | import rx.lang.scala.subjects.{ BehaviorSubject, AsyncSubject, ReplaySubject, PublishSubject } 22 | 23 | import scala.concurrent.{ Future, Promise } 24 | import scala.util.Success 25 | 26 | class PromisesAndSubjectsTest extends TestSpec { 27 | 28 | def futureOfInt: Future[Int] = { 29 | val p = Promise[Int]() 30 | p.complete(Success(42)) 31 | p.future 32 | } 33 | 34 | "Promise" should "return a future" in { 35 | futureOfInt.map(_ / 2).futureValue shouldBe 21 36 | } 37 | 38 | // a Subject represents an object that is both 39 | // an Observable and an Observer. 40 | 41 | // Rx has the following subjects: 42 | // AsyncSubject, BehaviorSubject, PublishSubject, 43 | // ReplaySubject, SerializedSubject, TestSubject 44 | 45 | /** 46 | * A PublishSubject is a Subject that, once an Observer has subscribed, 47 | * emits all subsequently observed items to the subscriber. 48 | * 49 | * see: http://reactivex.io/RxJava/javadoc/rx/subjects/PublishSubject.html 50 | */ 51 | "PublishSubject" should "return an Observable" in { 52 | val channel = PublishSubject[Int]() 53 | 54 | var xsA: List[Int] = Nil 55 | var xsB: List[Int] = Nil 56 | var xsC: List[Int] = Nil 57 | 58 | val a = channel.subscribe(x ⇒ xsA = x :: xsA) 59 | val b = channel.subscribe(x ⇒ xsB = x :: xsB) 60 | 61 | // explicitly call onNext 62 | channel.onNext(42) // put 42 on the channel 63 | a.unsubscribe() 64 | xsA shouldBe List(42) 65 | xsB shouldBe List(42) 66 | 67 | channel.onNext(4711) // put 4711 on the channel 68 | channel.onCompleted() 69 | xsA shouldBe List(42) 70 | xsB shouldBe List(4711, 42) 71 | 72 | val c = channel.subscribe(x ⇒ xsC = x :: xsC) 73 | channel.onNext(13) // you cannot put 13 on the channel, it already has been closed/completed 74 | xsA shouldBe List(42) 75 | xsB shouldBe List(4711, 42) 76 | xsC shouldBe Nil 77 | } 78 | 79 | /** 80 | * Subject that buffers all items it observes and replays 81 | * them to any Observer that subscribes. 82 | * 83 | * see: http://reactivex.io/RxJava/javadoc/rx/subjects/ReplaySubject.html 84 | */ 85 | "ReplaySubject" should "replay history" in { 86 | val channel = ReplaySubject[Int](3) 87 | 88 | var xsA: List[Int] = Nil 89 | var xsB: List[Int] = Nil 90 | var xsC: List[Int] = Nil 91 | 92 | val a = channel.subscribe(x ⇒ xsA = x :: xsA) 93 | val b = channel.subscribe(x ⇒ xsB = x :: xsB) 94 | 95 | // explicitly call onNext 96 | (1 to 3).foreach(channel.onNext) 97 | xsA shouldBe List(3, 2, 1) 98 | xsB shouldBe List(3, 2, 1) 99 | a.unsubscribe() 100 | b.unsubscribe() 101 | 102 | channel.onCompleted() // close the channel 103 | val c = channel.subscribe(x ⇒ xsC = x :: xsC) 104 | channel.onNext(13) // you cannot put 13 on the channel, it already has been closed/completed 105 | xsC shouldBe List(3, 2, 1) // but the ReplaySubject has a buffer of 3, so they get placed on the channel 106 | } 107 | 108 | /** 109 | * Subject that publishes only the last item observed to each Observer 110 | * that has subscribed, when the source Observable completes. 111 | * 112 | * see: http://reactivex.io/RxJava/javadoc/rx/subjects/AsyncSubject.html 113 | */ 114 | "AsyncSubject" should "return the last item" in { 115 | val channel = AsyncSubject[Int]() 116 | 117 | var xsA: List[Int] = Nil 118 | var xsB: List[Int] = Nil 119 | var xsC: List[Int] = Nil 120 | 121 | val a = channel.subscribe(x ⇒ xsA = x :: xsA) 122 | val b = channel.subscribe(x ⇒ xsB = x :: xsB) 123 | 124 | // explicitly call onNext 125 | (1 to 3).foreach(channel.onNext) 126 | channel.onCompleted() // the channel must be closed! see the docs 127 | xsA shouldBe List(3) 128 | xsB shouldBe List(3) 129 | a.unsubscribe() 130 | b.unsubscribe() 131 | 132 | val c = channel.subscribe(x ⇒ xsC = x :: xsC) 133 | channel.onNext(13) // you cannot put 13 on the channel, it already has been closed/completed 134 | xsC shouldBe List(3) // returns the last item observed to the observer 135 | } 136 | 137 | /** 138 | * Subject that emits the most recent item it has observed and all 139 | * subsequent observed items to each subscribed Observer. 140 | * 141 | * see: http://reactivex.io/RxJava/javadoc/rx/subjects/BehaviorSubject.html 142 | */ 143 | "BehaviorSubject" should "most recent item" in { 144 | val channel = BehaviorSubject[Int]() 145 | 146 | var xsA: List[Int] = Nil 147 | var xsB: List[Int] = Nil 148 | var xsC: List[Int] = Nil 149 | 150 | val a = channel.subscribe(x ⇒ xsA = x :: xsA) 151 | channel.onNext(1) 152 | channel.onNext(2) 153 | val b = channel.subscribe(x ⇒ xsB = x :: xsB) 154 | channel.onNext(3) 155 | xsA shouldBe List(3, 2, 1) 156 | xsB shouldBe List(3, 2) 157 | a.unsubscribe() 158 | b.unsubscribe() 159 | 160 | val c = channel.subscribe(x ⇒ xsC = x :: xsC) 161 | channel.onNext(4) 162 | xsC shouldBe List(4, 3) 163 | channel.onCompleted() 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week4/RxOperatorsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week4 18 | 19 | import com.test.TestSpec 20 | import rx.lang.scala._ 21 | 22 | import scala.concurrent.duration._ 23 | 24 | class RxOperatorsTest extends TestSpec { 25 | 26 | /** 27 | * Observables are asynchronous streams of data. 28 | * 29 | * Contrary to Futures, they can return multiple values. 30 | * 31 | * In ReactiveX an `observer` subscribes to an `Observable`. 32 | * 33 | * Then that observer `reacts` to whatever item or sequence 34 | * of items the Observable `emits`. 35 | * 36 | * So, the most important part is that Observable(s) emit 37 | * items (stuff) that observers can subscribe to and react 38 | * upon. 39 | * 40 | * The workflow is: 41 | * 1. Create an Observable (note, it emits items!!) 42 | * 2. write an 'item'-processing pipeline, eg. 43 | * take (2) items, then convert those to a list 44 | * 3. Subscribe to the observable 45 | * 4. React to the emitted and transformed items 46 | */ 47 | 48 | /** 49 | * Create an observable that emits 0, 1, 2, ... with a delay 50 | * of duration between consecutive numbers. 51 | */ 52 | def observableThatEmitsNumbers: Observable[Long] = Observable.interval(200.millis) 53 | 54 | /** 55 | * Create an observable that emits no data to the observer 56 | * and immediately invokes its onCompleted method. 57 | */ 58 | def emptyObservable: Observable[Nothing] = Observable.empty 59 | 60 | "Observer that emits numbers" should "emit the number 0" in { 61 | val observable = 62 | observableThatEmitsNumbers 63 | // the observable will take 2 items from the stream 64 | // then it will automatically unsubscribe 65 | .take(2) 66 | // for testing, it is better to have a BlockingObservable, normally 67 | // all operations are asynchronous and non-blocking 68 | .toBlocking 69 | 70 | // Subscribe to the observable and take the head of the stream, when there are 71 | // no items, return None 72 | // Returns an Option with the very first item emitted by the source 73 | // Observable, or None if the source Observable is empty. 74 | val result: Option[Long] = observable.headOption 75 | result.headOption should not be empty 76 | result.value shouldBe 0 77 | } 78 | 79 | it should "emit 0, 1" in { 80 | // the `.toList` returns an Observable that emits a single item, a List composed of all the items emitted by the 81 | // source Observable. Be careful not to use this operator on Observables that emit infinite or very large numbers 82 | // of items, as you do not have the option to unsubscribe. 83 | observableThatEmitsNumbers 84 | .take(2) 85 | .toBlocking 86 | .toList shouldBe List(0, 1) 87 | } 88 | 89 | it should "transform the items using .map()" in { 90 | observableThatEmitsNumbers 91 | .drop(3) 92 | .take(2) 93 | .map(n ⇒ "number: " + n) 94 | .toBlocking 95 | .toList shouldBe List("number: 3", "number: 4") 96 | } 97 | 98 | it should "merge two streams" in { 99 | val o1 = observableThatEmitsNumbers.drop(3).take(3) // 3, 4, 5 100 | val o2 = observableThatEmitsNumbers.take(3) // 0, 1, 2 101 | o1.merge(o2) 102 | .take(6) 103 | .toBlocking 104 | .toList shouldBe List(0, 1, 2, 3, 4, 5) 105 | } 106 | 107 | "An empty observable" should "emit nothing" in { 108 | emptyObservable 109 | .toBlocking 110 | .headOption shouldBe empty 111 | } 112 | 113 | it should "return an empty list" in { 114 | emptyObservable.toList.toBlocking.head shouldBe empty 115 | emptyObservable.toList.toBlocking.head shouldBe Nil 116 | } 117 | 118 | "A list" should "convert to Observable" in { 119 | val o1 = List(0, 1, 2).toObservable 120 | val o2 = List(3, 4, 5).toObservable 121 | o1.merge(o2) 122 | .take(6) 123 | .toBlocking 124 | .toList shouldBe List(0, 1, 2, 3, 4, 5) 125 | } 126 | 127 | it should "convert to Observable in reverse" in { 128 | val o1 = List(0, 1, 2).toObservable 129 | val o2 = List(3, 4, 5).toObservable 130 | o2.merge(o1) 131 | .take(6) 132 | .toBlocking 133 | .toList shouldBe List(3, 4, 5, 0, 1, 2) 134 | } 135 | 136 | "Observables" should "be used as follows" in { 137 | observableThatEmitsNumbers // emit numbers 138 | .slidingBuffer(count = 2, skip = 1) // buffer 2 elements, skip 1, so (0, 1), (1, 2), (2, 3) etc 139 | .take(3) // take 2 pairs, then unsubscribe automatically 140 | .toBlocking 141 | .toList should contain inOrder (Seq(0, 1), Seq(1, 2)) 142 | } 143 | 144 | it should "filter" in { 145 | observableThatEmitsNumbers // emit numbers 146 | .filter(_ % 2 == 0) // only emit elements that are even numbers 147 | .slidingBuffer(count = 2, skip = 2) // buffer 2 elements and skip 2 (0, 2), (4, 6) etc 148 | .take(2) // take 2 pairs 149 | .toBlocking 150 | .toList should contain inOrder (Seq(0, 2), Seq(4, 6)) 151 | } 152 | 153 | it should "flatMap" in { 154 | val o1 = observableThatEmitsNumbers.take(2) 155 | val o2 = observableThatEmitsNumbers.take(5) 156 | o1.flatMap(_ ⇒ o2) 157 | .toBlocking 158 | .toList should not be empty 159 | 160 | // the content of the resulting list is non-deterministic. 161 | // this is because, in contrary to iterables, observables are asynchronous 162 | // the function you are flat-mapping over will produce its values asynchronously 163 | } 164 | 165 | it should "merge" in { 166 | val o1 = observableThatEmitsNumbers.take(2) 167 | val o2 = observableThatEmitsNumbers.take(5) 168 | o1.merge(o2) 169 | .toBlocking 170 | .toList shouldBe List(0, 0, 1, 1, 2, 3, 4) 171 | } 172 | 173 | it should "concat" in { 174 | val o1 = observableThatEmitsNumbers.take(2) 175 | val o2 = observableThatEmitsNumbers.take(5) 176 | (o1 ++ o2) 177 | .toBlocking 178 | .toList shouldBe List(0, 1, 0, 1, 2, 3, 4) 179 | } 180 | 181 | it should "sum" in { 182 | observableThatEmitsNumbers 183 | .take(5) 184 | .sum 185 | .toBlocking 186 | .head shouldBe 10 187 | } 188 | 189 | it should "count" in { 190 | observableThatEmitsNumbers 191 | .take(5) 192 | .countLong 193 | .toBlocking 194 | .head shouldBe 5 195 | } 196 | 197 | it should "zip" in { 198 | val o1 = observableThatEmitsNumbers.take(3) 199 | val o2 = observableThatEmitsNumbers.drop(5).take(5) 200 | o1.zip(o2) 201 | .toBlocking 202 | .toList shouldBe List((0, 5), (1, 6), (2, 7)) 203 | } 204 | 205 | // The marble diagram of the sheet is wrong, the three observables 206 | // each emit only 3's, or 2's or 1's not zero and ones 207 | "flattening nested streams" should "return the correct sequence" in { 208 | val xs: Observable[Int] = Observable.from(List(3, 2, 1)) 209 | val yss: Observable[Observable[Int]] = 210 | xs.map(x ⇒ Observable.interval(x seconds).map(_ ⇒ x).take(2)) 211 | val zs: Observable[Int] = yss.flatten 212 | 213 | zs.toBlocking.toList match { 214 | case List(1, 1, 2, 3, 2, 3) ⇒ 215 | case List(1, 2, 1, 3, 2, 3) ⇒ 216 | case u ⇒ fail("Unexpected: " + u) 217 | } 218 | } 219 | 220 | "Concatenating nested streams" should "return the correct sequence" in { 221 | Observable.from(List(3, 2, 1)) 222 | .map(x ⇒ Observable.interval(x seconds).map(_ ⇒ x).take(2)) 223 | .concat 224 | .toBlocking 225 | .toList shouldBe List(3, 3, 2, 2, 1, 1) 226 | 227 | // note, never use concat, because it must wait until all streams terminate 228 | // before the streams can be concatenated. 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week4/SubscriptionsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week4 18 | 19 | import com.test.TestSpec 20 | import rx.lang.scala._ 21 | import rx.lang.scala.subscriptions.{ SerialSubscription, MultipleAssignmentSubscription, CompositeSubscription } 22 | import scala.concurrent.duration._ 23 | 24 | class SubscriptionsTest extends TestSpec { 25 | 26 | "Streams" should "manually unsubscribe" in { 27 | val s1: Subscription = Observable.interval(1.second).subscribe() 28 | val s2: Subscription = Observable.interval(2.seconds).subscribe() 29 | s1 should not be 'unsubscribed 30 | s2 should not be 'unsubscribed 31 | s1.unsubscribe() 32 | s1 should be('unsubscribed) 33 | s2 should not be 'unsubscribed 34 | s2.unsubscribe() 35 | s2 should be('unsubscribed) 36 | 37 | // s1 and s2 are both 'cold' observables, 38 | // because they each have their own private 39 | // data source 40 | 41 | // 'hot' observables all share the same data source 42 | // so all subscribers are all shared together 43 | } 44 | 45 | "Subscription" should "unsubscribe idempotently" in { 46 | var counter = 0 47 | val s1 = Subscription { 48 | counter += 1 49 | } 50 | s1 should not be 'unsubscribed 51 | s1.unsubscribe() 52 | s1 should be('unsubscribed) 53 | s1.unsubscribe() 54 | counter shouldBe 1 55 | } 56 | 57 | "CompositeSubscription" should "unsubscribe both subscriptions" in { 58 | val s1 = Subscription() 59 | val s2 = Subscription() 60 | // Subscription that represents a group of Subscriptions that are unsubscribed together. 61 | val composite = CompositeSubscription(s1, s2) 62 | s1 should not be 'unsubscribed 63 | s2 should not be 'unsubscribed 64 | composite should not be 'unsubscribed 65 | composite.unsubscribe() 66 | s1 should be('unsubscribed) 67 | s2 should be('unsubscribed) 68 | } 69 | 70 | "MultipleAssignmentSubscription" should "unsubscribe" in { 71 | val s1 = Subscription() 72 | val s2 = Subscription() 73 | 74 | val multi = MultipleAssignmentSubscription() 75 | multi should not be 'unsubscribed 76 | s1 should not be 'unsubscribed 77 | s2 should not be 'unsubscribed 78 | 79 | // set the underlying subscription of multi to s1 80 | multi.subscription = s1 81 | // unsubscribe multi 82 | multi.unsubscribe() 83 | multi should be('unsubscribed) 84 | s1 should be('unsubscribed) 85 | s2 should not be 'unsubscribed 86 | 87 | // so multi is unsubscribed.. now set the underlying 88 | // subscription of multi to s2, which is still subscribed, 89 | // see what happens 90 | multi.subscription = s2 91 | multi should be('unsubscribed) 92 | s1 should be('unsubscribed) 93 | s2 should be('unsubscribed) 94 | } 95 | 96 | "SerialSubscription" should "unsubscribe" in { 97 | val s1 = Subscription() 98 | val s2 = Subscription() 99 | 100 | // SerialSubscription Represents a subscription whose underlying 101 | // subscription can be swapped for another subscription which 102 | // causes the previous underlying subscription to be unsubscribed. 103 | val serial = SerialSubscription() 104 | serial.subscription = s1 105 | serial should not be 'unsubscribed 106 | s1 should not be 'unsubscribed 107 | s2 should not be 'unsubscribed 108 | serial.subscription = s2 109 | serial should not be 'unsubscribed 110 | s1 should be('unsubscribed) 111 | s2 should not be 'unsubscribed 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week4/Usgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week4 18 | 19 | import rx.lang.scala.Observable 20 | 21 | import scala.concurrent.{ Promise, Future } 22 | import scala.util.Random 23 | 24 | object Usgs { 25 | 26 | lazy val rnd = new Random() 27 | 28 | case class EarthQuake(magnitude: Double, location: (Double, Double)) 29 | case class Country(name: String) 30 | 31 | /** 32 | * A very very inaccurate and course grained geo tranlation table, please forgive me :) 33 | */ 34 | def reverseGeoCode(c: (Double, Double)): Future[Country] = { 35 | val p = Promise[Country]() 36 | val country = c match { 37 | case (a, b) if (0 to 50).contains(a.toInt) && (0 to 20).contains(b.toInt) ⇒ Country("United Kingdom") 38 | case (a, b) if (0 to 50).contains(a.toInt) && (20 to 40).contains(b.toInt) ⇒ Country("Germany") 39 | case (a, b) if (0 to 50).contains(a.toInt) && (40 to 60).contains(b.toInt) ⇒ Country("Belgium") 40 | case (a, b) if (0 to 50).contains(a.toInt) && (60 to 80).contains(b.toInt) ⇒ Country("France") 41 | case (a, b) if (0 to 50).contains(a.toInt) && (80 to 100).contains(b.toInt) ⇒ Country("Spain") 42 | case (a, b) if (50 to 100).contains(a.toInt) && (0 to 20).contains(b.toInt) ⇒ Country("America") 43 | case (a, b) if (50 to 100).contains(a.toInt) && (20 to 40).contains(b.toInt) ⇒ Country("Canada") 44 | case (a, b) if (50 to 100).contains(a.toInt) && (40 to 60).contains(b.toInt) ⇒ Country("Mexico") 45 | case (a, b) if (50 to 100).contains(a.toInt) && (60 to 80).contains(b.toInt) ⇒ Country("Colombia") 46 | case (a, b) if (50 to 100).contains(a.toInt) && (80 to 100).contains(b.toInt) ⇒ Country("Brazil") 47 | case _ ⇒ Country("The Netherlands") 48 | } 49 | p.success(country) 50 | p.future 51 | } 52 | 53 | trait Magnitude { def min: Int; def max: Int } 54 | case object Micro extends Magnitude { val min = 0; val max = 5 } 55 | case object Minor extends Magnitude { val min = 5; val max = 10 } 56 | case object Light extends Magnitude { val min = 10; val max = 30 } 57 | case object Moderate extends Magnitude { val min = 30; val max = 60 } 58 | case object Strong extends Magnitude { val min = 60; val max = 70 } 59 | case object Major extends Magnitude { val min = 70; val max = 90 } 60 | case object Great extends Magnitude { val min = 90; val max = 100 } 61 | 62 | /** 63 | * This is not a correct translation, but hey... 64 | */ 65 | object Magnitude { 66 | def apply(mag: Int): Magnitude = mag match { 67 | case _ if (Micro.min to Micro.max).contains(mag) ⇒ Micro 68 | case _ if (Minor.min to Minor.max).contains(mag) ⇒ Minor 69 | case _ if (Light.min to Light.max).contains(mag) ⇒ Light 70 | case _ if (Moderate.min to Moderate.max).contains(mag) ⇒ Moderate 71 | case _ if (Strong.min to Strong.max).contains(mag) ⇒ Strong 72 | case _ if (Major.min to Major.max).contains(mag) ⇒ Major 73 | case _ ⇒ Great 74 | } 75 | } 76 | 77 | /** 78 | * Generate a random EarthQuake 79 | */ 80 | def generateEarthQuake: EarthQuake = 81 | EarthQuake(magnitude = rnd.nextDouble * 100, location = (rnd.nextDouble * 100, rnd.nextDouble * 100)) 82 | 83 | /** 84 | * Return a continuous stream of EarthQuakes 85 | * @return 86 | */ 87 | def stream: Observable[EarthQuake] = Observable(observer ⇒ { 88 | try { 89 | while (!observer.isUnsubscribed) { 90 | observer.onNext(generateEarthQuake) 91 | } 92 | observer.onCompleted() 93 | } catch { 94 | case ex: Throwable ⇒ observer.onError(ex) 95 | } 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week5/AsyncHttoClientTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week5 18 | 19 | import com.github.dnvriend.HttpClient._ 20 | import com.github.dnvriend.HttpUtils._ 21 | import com.github.dnvriend.HttpClient 22 | import com.test.TestSpec 23 | 24 | class AsyncHttoClientTest extends TestSpec { 25 | 26 | "AsyncHttpClient" should "get info from google" in { 27 | HttpClient() 28 | .get("http://www.google.nl") 29 | .links 30 | .futureValue 31 | .value should not be empty 32 | } 33 | 34 | it should "print all to sysout" in { 35 | HttpClient() 36 | .get("http://www.google.nl") 37 | .links 38 | .futureValue 39 | .value 40 | .foreach(println) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week5/BankAccountTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week5 18 | 19 | import akka.actor.Status.Failure 20 | import akka.actor.{ Actor, ActorRef, Props } 21 | import akka.event.LoggingReceive 22 | import akka.pattern.ask 23 | import com.test.TestSpec 24 | 25 | class BankAccountTest extends TestSpec { 26 | 27 | "Actors" should "know itself" in { 28 | val ref: ActorRef = system.actorOf(Props(new Actor { 29 | override def receive: Receive = { 30 | case _ ⇒ sender() ! self 31 | } 32 | })) 33 | 34 | (ref ? "").futureValue shouldBe ref 35 | cleanup(ref) 36 | } 37 | 38 | it should "count" in { 39 | val ref: ActorRef = system.actorOf(Props(new Actor { 40 | def count(num: Int): Receive = LoggingReceive { 41 | case _ ⇒ 42 | context.become(count(num + 1)) 43 | sender() ! num 44 | } 45 | override def receive: Receive = LoggingReceive(count(0)) 46 | })) 47 | (ref ? "").futureValue shouldBe 0 48 | (ref ? "").futureValue shouldBe 1 49 | (ref ? "").futureValue shouldBe 2 50 | (ref ? "").futureValue shouldBe 3 51 | cleanup(ref) 52 | } 53 | 54 | it should "be a BankAccount" in { 55 | object BankAccount { 56 | case class Transfer(from: ActorRef, to: ActorRef, amount: BigInt) 57 | case class Deposit(amount: BigInt) 58 | case class Withdraw(amount: BigInt) 59 | case object Info 60 | case class Done(amount: BigInt) 61 | case object Failed 62 | } 63 | class BankAccount extends Actor { 64 | import BankAccount._ 65 | var balance: BigInt = BigInt(0) 66 | override def receive: Receive = LoggingReceive { 67 | case Deposit(amount) ⇒ 68 | balance += amount 69 | sender() ! Done(balance) 70 | case Withdraw(amount) ⇒ 71 | balance -= amount 72 | sender() ! Done(balance) 73 | case Info ⇒ 74 | sender() ! Done(balance) 75 | case _ ⇒ sender() ! Failure 76 | } 77 | } 78 | 79 | import BankAccount._ 80 | val account1 = system.actorOf(Props(new BankAccount)) 81 | (account1 ? Info).futureValue shouldBe Done(0) 82 | (account1 ? Deposit(100)).futureValue shouldBe Done(100) 83 | (account1 ? Deposit(100)).futureValue shouldBe Done(200) 84 | 85 | val account2 = system.actorOf(Props(new BankAccount)) 86 | 87 | val tom = system.actorOf(Props(new Actor { 88 | def awaitDeposit(client: ActorRef): Receive = LoggingReceive { 89 | case Done(amount) ⇒ 90 | client ! Done(amount) 91 | context.stop(self) 92 | } 93 | def awaitWithdraw(to: ActorRef, amount: BigInt, client: ActorRef): Receive = LoggingReceive { 94 | case Done(_) ⇒ 95 | to ! Deposit(amount) 96 | context.become(awaitDeposit(client)) 97 | case Failed ⇒ 98 | client ! Failed 99 | context.stop(self) 100 | } 101 | override def receive = { 102 | case Transfer(from, to, amount) ⇒ 103 | from ! Withdraw(amount) 104 | context.become(awaitWithdraw(to, amount, sender())) 105 | } 106 | })) 107 | 108 | (tom ? Transfer(account1, account2, 50)).futureValue shouldBe Done(50) 109 | (account1 ? Info).futureValue shouldBe Done(150) 110 | (account2 ? Info).futureValue shouldBe Done(50) 111 | cleanup(account1, account2, tom) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week5/CounterTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week5 18 | 19 | import akka.actor.{ Actor, Props } 20 | import akka.event.LoggingReceive 21 | import com.test.TestSpec 22 | 23 | class CounterTest extends TestSpec { 24 | 25 | class Counter extends Actor { 26 | 27 | def counter(n: Int): Receive = LoggingReceive { 28 | case "incr" ⇒ context.become(counter(n + 1)) 29 | case "get" ⇒ sender() ! n 30 | } 31 | 32 | override def receive = counter(0) 33 | } 34 | 35 | "counter" should "increment" in { 36 | val counter = system.actorOf(Props(new Counter), "counter") 37 | val p = probe 38 | p.send(counter, "incr") 39 | p.send(counter, "get") 40 | p.expectMsg(1) 41 | p.send(counter, "incr") 42 | p.send(counter, "get") 43 | p.expectMsg(2) 44 | cleanup(counter) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week5/LinkCheckerTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week5 18 | 19 | import akka.actor.Status.Failure 20 | import akka.actor._ 21 | import akka.event.LoggingReceive 22 | import akka.pattern._ 23 | import com.github.dnvriend.{ HttpUtils, HttpClient } 24 | import com.github.dnvriend.HttpClient._ 25 | import com.test.TestSpec 26 | 27 | import scala.concurrent.Future 28 | import scala.concurrent.duration._ 29 | 30 | class LinkCheckerTest extends TestSpec { 31 | object Getter { 32 | case object Done 33 | case object Abort 34 | } 35 | class Getter(url: String, depth: Int) extends Actor with ActorLogging { 36 | import Getter._ 37 | implicit val ec = context.dispatcher 38 | 39 | HttpClient() get url pipeTo self 40 | 41 | override def receive: Receive = LoggingReceive { 42 | case body: String ⇒ 43 | HttpUtils.findLinks(body).foreach { links ⇒ 44 | for (link ← links) { 45 | context.parent ! Controller.Check(link, depth) 46 | } 47 | } 48 | stop() 49 | case Failure(t) ⇒ 50 | stop() 51 | case Abort ⇒ 52 | stop() 53 | } 54 | 55 | def stop(): Unit = { 56 | context.parent ! Getter.Done 57 | context.stop(self) 58 | } 59 | 60 | override def preStart(): Unit = 61 | log.info("Starting: {}", self.path) 62 | 63 | override def postStop(): Unit = 64 | log.info("Stopping: {}", self.path) 65 | 66 | } 67 | 68 | object Controller { 69 | case class Check(link: String, depth: Int) 70 | case class Result(cache: Set[String]) 71 | } 72 | 73 | class Controller extends Actor with ActorLogging { 74 | import Controller._ 75 | 76 | var cache = Set.empty[String] // stores the result url 77 | var children = Set.empty[ActorRef] 78 | 79 | context.system.scheduler.scheduleOnce(10.seconds, self, ReceiveTimeout) 80 | 81 | override def receive = LoggingReceive { 82 | case Check(url, depth) ⇒ 83 | if (!cache(url) && depth > 0) 84 | children += context.actorOf(Props(new Getter(url, depth - 1)), s"getter-$randomId") 85 | cache += url 86 | case Getter.Done ⇒ 87 | children -= sender 88 | if (children.isEmpty) { 89 | context.parent ! Result(cache) 90 | } 91 | case ReceiveTimeout ⇒ 92 | children.foreach { _ ! Getter.Abort } 93 | } 94 | 95 | override def preStart(): Unit = 96 | log.info("Starting: {}", self.path) 97 | 98 | override def postStop(): Unit = 99 | log.info("Stopping: {}", self.path) 100 | } 101 | 102 | object Receptionist { 103 | case class Failed(url: String) 104 | case class Get(url: String) 105 | case class Result(url: String, links: Set[String]) 106 | } 107 | 108 | class Receptionist extends Actor with ActorLogging { 109 | import Receptionist._ 110 | case class Job(client: ActorRef, url: String) 111 | 112 | var reqNo = 0 113 | 114 | def runNext(queue: Vector[Job]): Receive = LoggingReceive { 115 | reqNo += 1 116 | if (queue.isEmpty) { 117 | log.info("Queue is empty, waiting for jobs") 118 | waiting 119 | } else { 120 | val controller = context.actorOf(Props(new Controller), s"c$reqNo") 121 | controller ! Controller.Check(queue.head.url, 2) 122 | log.info("Running job: {}", queue.head) 123 | running(queue) 124 | } 125 | } 126 | 127 | def enqueueJob(queue: Vector[Job], job: Job): Receive = LoggingReceive { 128 | if (queue.size > 3) { 129 | log.info("Cannot accept any more jobs: {}", job) 130 | sender ! Failed(job.url) // cannot accept any more jobs 131 | running(queue) 132 | } else { 133 | log.info("Enqueue job: {}", job) 134 | running(queue :+ job) 135 | } 136 | } 137 | 138 | val waiting: Receive = LoggingReceive { 139 | case Get(url) ⇒ 140 | context.become(runNext(Vector(Job(sender(), url)))) 141 | } 142 | 143 | def running(queue: Vector[Job]): Receive = LoggingReceive { 144 | case Controller.Result(links) ⇒ 145 | val job = queue.head 146 | job.client ! Result(job.url, links) 147 | context.stop(sender()) 148 | context.become(runNext(queue.tail)) 149 | case Get(url) ⇒ 150 | context.become(enqueueJob(queue, Job(sender(), url))) 151 | } 152 | 153 | override def receive = waiting 154 | 155 | override def preStart(): Unit = 156 | log.info("Starting: {}", self.path) 157 | 158 | override def postStop(): Unit = 159 | log.info("Stopping: {}", self.path) 160 | } 161 | 162 | "Receptionist" should "place a request and get a respond" in { 163 | import Receptionist._ 164 | val receptionist = system.actorOf(Props(new Receptionist), "receptionist") 165 | (receptionist ? Get("http://www.google.com")).futureValue match { 166 | case Result(url, set) ⇒ 167 | println(set.toVector.sorted.mkString(s"Results for '$url:'\n", "\n", "\n")) 168 | case Failed(url) ⇒ 169 | println(s"Failed to fetch '$url'") 170 | } 171 | cleanup(receptionist) 172 | } 173 | 174 | it should "handle more work" in { 175 | import Receptionist._ 176 | val receptionist = system.actorOf(Props(new Receptionist), "receptionist") 177 | Future.sequence((1 to 10).map(_ ⇒ receptionist ? Get("http://www.google.com")).toList).futureValue 178 | cleanup(receptionist) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week5/RunningWithTestProbeTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week5 18 | 19 | import akka.actor._ 20 | import akka.event.LoggingReceive 21 | import akka.pattern._ 22 | import akka.testkit.{ ImplicitSender, TestKit, TestProbe } 23 | import akka.util.Timeout 24 | import org.scalatest.concurrent.ScalaFutures 25 | import org.scalatest.{ GivenWhenThen, BeforeAndAfterAll, FlatSpecLike, Matchers } 26 | 27 | import scala.concurrent.Future 28 | import scala.concurrent.duration._ 29 | import scala.util.Try 30 | 31 | class RunningWithTestProbeTest extends TestKit(ActorSystem("test")) with ImplicitSender with FlatSpecLike with BeforeAndAfterAll with ScalaFutures with Matchers with GivenWhenThen { 32 | 33 | "Toggle" should "toggle happy to sad and back" in { 34 | val toggle = system.actorOf(Props(new Actor { 35 | 36 | /** 37 | * Returns a partial function that represents the happy state 38 | */ 39 | def happy: Receive = LoggingReceive { 40 | case "How are you?" ⇒ 41 | sender() ! "happy" 42 | context.become(sad) 43 | } 44 | 45 | /** 46 | * Returns a partial function that represents the sad state 47 | */ 48 | def sad: Receive = LoggingReceive { 49 | case "How are you?" ⇒ 50 | sender() ! "sad" 51 | context.become(happy) 52 | } 53 | 54 | /** 55 | * Represents the initial actor state 56 | */ 57 | override def receive: Receive = happy 58 | })) 59 | toggle ! "How are you?" 60 | expectMsg("happy") 61 | toggle ! "How are you?" 62 | expectMsg("sad") 63 | toggle ! "unknown" 64 | expectNoMsg(1.second) 65 | cleanUp(toggle) 66 | } 67 | 68 | "WrongActor" should "sleep one second" in { 69 | val wrong = system.actorOf(Props(new Actor { 70 | override def receive: Actor.Receive = LoggingReceive { 71 | case "How are you?" ⇒ 72 | Thread.sleep(1000) // never ever do this in an actor!! 73 | sender() ! "I'm fine" 74 | } 75 | })) 76 | wrong ! "How are you?" 77 | expectMsg(2.seconds, "I'm fine") 78 | cleanUp(wrong) 79 | } 80 | 81 | "CorrectActor" should "sleep one second" in { 82 | val correct = system.actorOf(Props(new Actor { 83 | import context.dispatcher 84 | override def receive: Actor.Receive = { 85 | case "How are you?" ⇒ 86 | context.system.scheduler.scheduleOnce(1.second, sender(), "I'm fine") 87 | } 88 | })) 89 | correct ! "How are you?" 90 | expectMsg(2.seconds, "I'm fine") 91 | cleanUp(correct) 92 | } 93 | 94 | "Ask pattern future" should "succeed when an akka.actor.Status.Success message has been received" in { 95 | Given("A success actor that will return a akka.actor.Status.Success message when any message has been received") 96 | val success = system.actorOf(Props(new Actor { 97 | override def receive = LoggingReceive { 98 | case _ ⇒ sender() ! Status.Success("success!") 99 | } 100 | }), "SuccessActor") 101 | 102 | And("An implicit timeout of 500 milliseconds has been configured") 103 | implicit val timeout = Timeout(500.millis) 104 | 105 | When("A message 'Foo' has been send to the actor using the ask pattern") 106 | Then("The Future should succeed") 107 | success.ask(success, "Foo").toTry should be a 'success 108 | cleanUp(success) 109 | } 110 | 111 | it should "fail when an akka.actor.Status.Failure message has been received" in { 112 | Given("A failure actor that will return a akka.actor.Status.Failure message when any message has been received") 113 | val failure = system.actorOf(Props(new Actor { 114 | override def receive = LoggingReceive { 115 | case _ ⇒ sender() ! Status.Failure(new RuntimeException("This is an error")) 116 | } 117 | }), "FailureActor") 118 | 119 | And("An implicit timeout of 500 milliseconds has been configured") 120 | implicit val timeout = Timeout(500.millis) 121 | 122 | When("A message 'Foo' has been send to the actor using the ask pattern") 123 | Then("The Future should fail") 124 | val response = failure.ask(failure, "Foo").toTry 125 | response should be a 'failure 126 | response.failed.get.getMessage should include("The future returned an exception of type: java.lang.RuntimeException, with message: This is an error") 127 | cleanUp(failure) 128 | } 129 | 130 | it should "fail when the actor throws an RuntimeException, the future will time out" in { 131 | Given("A failure actor that will throw a RuntimeException when any message has been received") 132 | val exceptionActor = system.actorOf(Props(new Actor { 133 | override def receive = LoggingReceive { 134 | case _ ⇒ throw new RuntimeException("This is an error") 135 | } 136 | }), "RuntimeException") 137 | 138 | And("An implicit timeout of 500 milliseconds has been configured") 139 | implicit val timeout = Timeout(500.millis) 140 | 141 | When("A message 'Foo' has been send to the actor using the ask pattern") 142 | Then("The Future should time out") 143 | val response: Try[Any] = (exceptionActor ? "RuntimeException").toTry 144 | response should be a 'failure 145 | response.failed.get.getMessage should include("A timeout occurred waiting for a future to complete") 146 | cleanUp(exceptionActor) 147 | } 148 | 149 | it should "fail when the actor does not respond, the future will time out" in { 150 | Given("A non response actor that will never respond when a message had been received") 151 | val noResponseActor = system.actorOf(Props(new Actor { 152 | override def receive = LoggingReceive { 153 | case _ ⇒ 154 | } 155 | }), "NoResponseActor") 156 | 157 | And("An implicit timeout of 500 milliseconds has been configured") 158 | implicit val timeout = Timeout(500.millis) 159 | 160 | When("A message 'Foo' has been send to the actor using the ask pattern") 161 | Then("The Future should time out") 162 | val response: Try[Any] = (noResponseActor ? "NoResponseActor").toTry 163 | response should be a 'failure 164 | response.failed.get.getMessage should include("A timeout occurred waiting for a future to complete") 165 | cleanUp(noResponseActor) 166 | } 167 | 168 | override protected def afterAll(): Unit = { 169 | system.terminate() 170 | } 171 | 172 | def cleanUp(actors: ActorRef*): Unit = 173 | actors.foreach { (actor: ActorRef) ⇒ 174 | val probe = TestProbe() 175 | actor ! PoisonPill 176 | probe watch actor 177 | probe.expectTerminated(actor) 178 | } 179 | 180 | implicit class FutureToTry[T](f: Future[T]) { 181 | def toTry: Try[T] = Try(f.futureValue) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week6/AtLeastOnceDeliveryTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week6 18 | 19 | import akka.actor.{ PoisonPill, Props, ActorPath } 20 | import akka.event.LoggingReceive 21 | import akka.persistence.{ AtLeastOnceDelivery, PersistentActor } 22 | import com.test.TestSpec 23 | import akka.pattern.ask 24 | 25 | class AtLeastOnceDeliveryTest extends TestSpec { 26 | 27 | /** 28 | * - Guaranteeing delivery means retrying until successful 29 | * - Retries are the sender's responsibility 30 | * - The recipient needs to acknowledge receipt 31 | * - Lost receipts lead to duplicate deliveries 32 | * => at-least-once, so 0, or more times delivery by the sender, 33 | * so 0, or more times acknowledge by the recipient 34 | */ 35 | 36 | /** 37 | * - At-least-once delivery needs the sender and the receiver to collaborate 38 | * - Retrying means taking note that the message needs to be sent 39 | * - Acknowledgement means taking note of the receipt of the confirmation 40 | */ 41 | 42 | /** 43 | * - Performing the effect and persisting that it was done cannot be atomic 44 | * - Perform it before persisting for at-least-once semantic 45 | * - Perform it after persisting for at-most-once semantic 46 | * 47 | * - The choice needs to be made based on the underlying business model. 48 | * - A processing is idempotent then using at-least-once semantic achieves 49 | * effectively exactly-once processing 50 | */ 51 | 52 | sealed trait Protocol 53 | case class PublishPost(text: String, id: Long) extends Protocol 54 | case class PostPublished(id: Long) extends Protocol 55 | 56 | sealed trait Event 57 | case class PostCreated(text: String) extends Event 58 | 59 | // the test thread will play a user that posts a message 60 | sealed trait Api 61 | case class NewPost(text: String, id: Long) extends Api 62 | case class BlogPosted(id: Long) extends Api 63 | case object NrPosted 64 | case class NrPostedResponse(posted: Long) 65 | 66 | // The userActor will instruct the publisher to publish a post, 67 | // but the publisher will only do so, when it receives the PublishPost command 68 | 69 | class UserActor(subscriber: ActorPath) extends PersistentActor with AtLeastOnceDelivery { 70 | override def persistenceId: String = "userActor" 71 | 72 | override def receiveCommand: Receive = LoggingReceive { 73 | case NewPost(text, id) ⇒ 74 | persist(PostCreated(text)) { e ⇒ 75 | deliver(subscriber)(PublishPost(text, _)) 76 | sender() ! BlogPosted(id) 77 | } 78 | case PostPublished(id) ⇒ 79 | confirmDelivery(id) 80 | persist(PostPublished(id))(_ ⇒ ()) 81 | } 82 | 83 | override def receiveRecover: Receive = LoggingReceive { 84 | case PostCreated(text) ⇒ deliver(subscriber)(PublishPost(text, _)) 85 | case PostPublished(id) ⇒ confirmDelivery(id) 86 | } 87 | } 88 | 89 | class Publisher extends PersistentActor { 90 | override val persistenceId: String = "publisher" 91 | 92 | var expectedId = 1L 93 | 94 | var nrPosted = 0L 95 | 96 | override def receiveRecover: Receive = LoggingReceive { 97 | case PostPublished(id) ⇒ 98 | expectedId = id + 1 99 | nrPosted += 1 100 | } 101 | 102 | override def receiveCommand: Receive = LoggingReceive { 103 | case PublishPost(text, id) if id > expectedId ⇒ 104 | // ignore the message, the sender will retry 105 | 106 | case PublishPost(text, id) if id < expectedId ⇒ 107 | // already received, just confirm 108 | sender() ! PostPublished(id) 109 | 110 | case PublishPost(text, id) if id == expectedId ⇒ 111 | persist(PostPublished(id)) { e ⇒ 112 | sender() ! e 113 | // modify the website 114 | nrPosted += 1 115 | expectedId += 1 116 | } 117 | 118 | case NrPosted ⇒ sender() ! NrPostedResponse(nrPosted) 119 | } 120 | } 121 | 122 | "UserActor" should "Retry sending PublishPost command to Publisher" in { 123 | var publisher = system.actorOf(Props(new Publisher), "publisher") 124 | var userActor = system.actorOf(Props(new UserActor(publisher.path)), "userActor") 125 | val tp = probe 126 | tp watch publisher 127 | tp watch userActor 128 | (publisher ? NrPosted).futureValue shouldBe NrPostedResponse(posted = 0) 129 | (userActor ? NewPost("foo", 1)).futureValue shouldBe BlogPosted(1) 130 | (userActor ? NewPost("bar", 2)).futureValue shouldBe BlogPosted(2) 131 | (publisher ? NrPosted).futureValue shouldBe NrPostedResponse(posted = 2) 132 | publisher ! PoisonPill 133 | tp.expectTerminated(publisher) 134 | userActor ! PoisonPill 135 | tp.expectTerminated(userActor) 136 | publisher = system.actorOf(Props(new Publisher), "publisher") 137 | userActor = system.actorOf(Props(new UserActor(publisher.path)), "userActor") 138 | (publisher ? NrPosted).futureValue shouldBe NrPostedResponse(posted = 2) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week6/BlogPostTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week6 18 | 19 | import akka.actor.{ PoisonPill, Props } 20 | import akka.event.LoggingReceive 21 | import akka.pattern.ask 22 | import akka.persistence.PersistentActor 23 | import com.test.TestSpec 24 | 25 | class BlogPostTest extends TestSpec { 26 | 27 | sealed trait Event 28 | case class PostCreated(text: String) extends Event 29 | case object QuotaReached extends Event 30 | 31 | case class NewPost(text: String, id: Long) 32 | case class BlogPosted(id: Long) 33 | case class BlogNotPosted(id: Long, reason: String) 34 | 35 | case class State(posts: Vector[String], disabled: Boolean) { 36 | def update(event: Event): State = event match { 37 | case PostCreated(text) ⇒ copy(posts = posts :+ text) 38 | case QuotaReached ⇒ copy(disabled = true) 39 | } 40 | } 41 | 42 | class UserActor(pid: Long) extends PersistentActor { 43 | override val persistenceId: String = "user-" + pid // must be a stable id 44 | 45 | // state is encapsulated in a single case class that has all 46 | // logic to update the state 47 | var state = State(Vector.empty[String], disabled = false) 48 | 49 | def updateState(event: Event): Unit = { 50 | state = state.update(event) 51 | } 52 | 53 | override def receiveRecover: Receive = LoggingReceive { 54 | case event: Event ⇒ updateState(event) 55 | } 56 | 57 | override def receiveCommand: Receive = LoggingReceive { 58 | case NewPost(_, id) if state.disabled ⇒ 59 | sender() ! BlogNotPosted(id, "quota reached") 60 | 61 | case NewPost(text, id) if !state.disabled ⇒ 62 | persist(PostCreated(text)) { event ⇒ 63 | updateState(event) 64 | sender ! BlogPosted(id) // confirm 65 | } 66 | persist(QuotaReached)(updateState) 67 | } 68 | } 69 | 70 | class UserActorAsync(pid: Long) extends PersistentActor { 71 | override val persistenceId: String = "user-" + pid // must be a stable id 72 | 73 | // state is encapsulated in a single case class that has all 74 | // logic to update the state 75 | var state = State(Vector.empty[String], disabled = false) 76 | 77 | def updateState(event: Event): Unit = { 78 | state = state.update(event) 79 | } 80 | 81 | override def receiveRecover: Receive = LoggingReceive { 82 | case event: Event ⇒ updateState(event) 83 | } 84 | 85 | override def receiveCommand: Receive = LoggingReceive { 86 | case NewPost(_, id) if state.disabled ⇒ 87 | sender() ! BlogNotPosted(id, "quota reached") 88 | 89 | case NewPost(text, id) if !state.disabled ⇒ 90 | val created = PostCreated(text) 91 | updateState(created) 92 | updateState(QuotaReached) 93 | persistAsync(created)(_ ⇒ sender() ! BlogPosted(id)) 94 | persistAsync(QuotaReached)(_ ⇒ ()) 95 | persist(QuotaReached)(updateState) 96 | } 97 | } 98 | 99 | def createUser(pid: Long) = 100 | system.actorOf(Props(new UserActor(pid))) 101 | 102 | "BlogPost" should "store post and remember state" in { 103 | var user1 = createUser(1) 104 | (user1 ? NewPost("foo", 1)).futureValue shouldBe BlogPosted(1) 105 | (user1 ? NewPost("bar", 2)).futureValue shouldBe BlogNotPosted(2, "quota reached") 106 | user1 ! PoisonPill 107 | user1 = createUser(1) 108 | (user1 ? NewPost("bar", 2)).futureValue shouldBe BlogNotPosted(2, "quota reached") 109 | } 110 | 111 | "AsyncBlogPost" should "store post and remember state" in { 112 | var user1 = createUser(1) 113 | // because of async processing of events, the responses can be out of order 114 | val possilbleResponses: PartialFunction[Any, Unit] = { 115 | case BlogPosted(1) ⇒ 116 | case BlogNotPosted(1, "quota reached") ⇒ 117 | case BlogNotPosted(2, "quota reached") ⇒ 118 | } 119 | (user1 ? NewPost("foo", 1)).futureValue mustBe possilbleResponses 120 | (user1 ? NewPost("bar", 2)).futureValue mustBe possilbleResponses 121 | user1 ! PoisonPill 122 | user1 = createUser(1) 123 | (user1 ? NewPost("bar", 2)).futureValue mustBe possilbleResponses 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week6/DeathPactTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week6 18 | 19 | import akka.actor.Actor.emptyBehavior 20 | import akka.actor.{ Actor, ActorRef, PoisonPill, Props } 21 | import com.test.TestSpec 22 | 23 | class DeathPactTest extends TestSpec { 24 | 25 | // let's create some lovers 26 | 27 | class Boy(girl: ActorRef) extends Actor { 28 | context.watch(girl) // sign deathpact 29 | override def receive = emptyBehavior 30 | } 31 | 32 | class Girl extends Actor { 33 | import scala.concurrent.duration._ 34 | context.system.scheduler.scheduleOnce(100.millis, self, PoisonPill) 35 | override def receive: Receive = emptyBehavior 36 | } 37 | 38 | // yes I know, boy/girl, I am old fashioned.. 39 | 40 | "Lovers" should "die together" in { 41 | val tp = probe 42 | val girl = system.actorOf(Props(new Girl)) 43 | val boy = system.actorOf(Props(new Boy(girl))) 44 | tp watch boy 45 | tp watch girl 46 | tp.expectTerminated(girl) 47 | tp.expectTerminated(boy) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week6/LinkCheckerDeathWatchTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week6 18 | 19 | import akka.actor.Status.Failure 20 | import akka.actor._ 21 | import akka.event.LoggingReceive 22 | import akka.pattern._ 23 | import com.github.dnvriend.HttpClient._ 24 | import com.github.dnvriend.{ HttpClient, HttpUtils } 25 | import com.test.TestSpec 26 | 27 | import scala.concurrent.Future 28 | import scala.concurrent.duration._ 29 | 30 | class LinkCheckerDeathWatchTest extends TestSpec { 31 | object Getter { 32 | case object Abort 33 | } 34 | class Getter(url: String, depth: Int) extends Actor with ActorLogging { 35 | import Getter._ 36 | implicit val ec = context.dispatcher 37 | 38 | HttpClient() get url pipeTo self 39 | 40 | def stop(): Unit = context.stop(self) 41 | 42 | override def receive: Receive = LoggingReceive { 43 | case body: String ⇒ 44 | HttpUtils.findLinks(body).foreach { links ⇒ 45 | for (link ← links) 46 | context.parent ! Controller.Check(link, depth) 47 | } 48 | stop() 49 | case Failure(t) ⇒ stop() 50 | case Abort ⇒ stop() 51 | } 52 | } 53 | 54 | object Controller { 55 | case class Check(link: String, depth: Int) 56 | case class Result(cache: Set[String]) 57 | } 58 | 59 | class Controller extends Actor with ActorLogging { 60 | import Controller._ 61 | 62 | var cache = Set.empty[String] // stores the result url 63 | 64 | context.system.scheduler.scheduleOnce(10.seconds, self, ReceiveTimeout) 65 | 66 | override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(maxNrOfRetries = 5) { 67 | case _: Exception ⇒ SupervisorStrategy.Restart 68 | } 69 | 70 | override def receive = LoggingReceive { 71 | case Check(url, depth) ⇒ 72 | if (!cache(url) && depth > 0) 73 | context.watch(context.actorOf(Props(new Getter(url, depth - 1)))) 74 | cache += url 75 | 76 | case Terminated(_) ⇒ 77 | if (context.children.isEmpty) 78 | context.parent ! Result(cache) 79 | 80 | case ReceiveTimeout ⇒ context.children foreach context.stop 81 | } 82 | } 83 | 84 | object Receptionist { 85 | case class Failed(url: String) 86 | case class Get(url: String) 87 | case class Result(url: String, links: Set[String]) 88 | } 89 | 90 | class Receptionist extends Actor with ActorLogging { 91 | import Receptionist._ 92 | case class Job(client: ActorRef, url: String) 93 | 94 | override def supervisorStrategy: SupervisorStrategy = 95 | SupervisorStrategy.stoppingStrategy 96 | 97 | var reqNo = 0 98 | 99 | def runNext(queue: Vector[Job]): Receive = LoggingReceive { 100 | reqNo += 1 101 | if (queue.isEmpty) { 102 | log.info("Queue is empty, waiting for jobs") 103 | waiting 104 | } else { 105 | val controller = context.actorOf(Props(new Controller), s"c$reqNo") 106 | context.watch(controller) 107 | controller ! Controller.Check(queue.head.url, 2) 108 | log.info("Running job: {}", queue.head) 109 | running(queue) 110 | } 111 | } 112 | 113 | def enqueueJob(queue: Vector[Job], job: Job): Receive = LoggingReceive { 114 | if (queue.size > 3) { 115 | log.info("Cannot accept any more jobs: {}", job) 116 | sender ! Failed(job.url) // cannot accept any more jobs 117 | running(queue) 118 | } else { 119 | log.info("Enqueue job: {}", job) 120 | running(queue :+ job) 121 | } 122 | } 123 | 124 | val waiting: Receive = LoggingReceive { 125 | case Get(url) ⇒ 126 | context.become(runNext(Vector(Job(sender(), url)))) 127 | } 128 | 129 | def running(queue: Vector[Job]): Receive = LoggingReceive { 130 | case Controller.Result(links) ⇒ 131 | val job = queue.head 132 | job.client ! Result(job.url, links) 133 | context.stop(context.unwatch(sender())) 134 | context.become(runNext(queue.tail)) 135 | case Terminated(_) ⇒ 136 | val job = queue.head 137 | job.client ! Failed(job.url) 138 | context.become(runNext(queue.tail)) 139 | case Get(url) ⇒ 140 | context.become(enqueueJob(queue, Job(sender(), url))) 141 | } 142 | 143 | override def receive = waiting 144 | } 145 | 146 | "Receptionist" should "place a request and get a respond" in { 147 | import Receptionist._ 148 | val receptionist = system.actorOf(Props(new Receptionist), "receptionist") 149 | (receptionist ? Get("http://www.google.com")).futureValue match { 150 | case Result(url, set) ⇒ 151 | println(set.toVector.sorted.mkString(s"Results for '$url:'\n", "\n", "\n")) 152 | case Failed(url) ⇒ 153 | println(s"Failed to fetch '$url'") 154 | } 155 | cleanup(receptionist) 156 | } 157 | 158 | it should "handle more work" in { 159 | import Receptionist._ 160 | val receptionist = system.actorOf(Props(new Receptionist), "receptionist") 161 | Future.sequence((1 to 10).map(_ ⇒ receptionist ? Get("http://www.google.com")).toList).futureValue 162 | cleanup(receptionist) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/test/scala/com/test/week6/SupervisionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Dennis Vriend 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.test.week6 18 | 19 | import akka.actor._ 20 | import akka.event.LoggingReceive 21 | import akka.pattern.ask 22 | import akka.testkit.TestProbe 23 | import com.test.TestSpec 24 | 25 | import scala.concurrent.duration._ 26 | 27 | class SupervisionTest extends TestSpec { 28 | 29 | case class Command(f: () ⇒ Unit) 30 | case object Count 31 | case object GetState 32 | case class CounterState(counter: Long) 33 | 34 | class Supervisor(tp: TestProbe, svs: SupervisorStrategy) extends Actor { 35 | val worker: ActorRef = context.actorOf(Props(new Actor with ActorLogging { 36 | var counter = 0L 37 | 38 | override def receive: Receive = LoggingReceive { 39 | case Command(f) ⇒ f() 40 | case Count ⇒ counter += 1 41 | case GetState ⇒ sender() ! CounterState(counter) 42 | } 43 | 44 | override def preStart(): Unit = 45 | log.debug("Started") 46 | 47 | override def postStop(): Unit = 48 | log.debug("Stopped") 49 | }), "worker") 50 | tp watch worker 51 | 52 | override def receive = LoggingReceive { 53 | case msg ⇒ worker forward msg 54 | } 55 | 56 | override def supervisorStrategy: SupervisorStrategy = svs 57 | } 58 | 59 | def createSupervisor(tp: TestProbe)(svs: SupervisorStrategy) = 60 | system.actorOf(Props(new Supervisor(tp, svs)), s"sup-${randomId.take(3)}") 61 | 62 | "SupervisorStrategy" should "resume the worker, state should not change, so should be 1" in { 63 | val tp = probe 64 | val sup = createSupervisor(tp) { 65 | OneForOneStrategy() { 66 | case t: RuntimeException ⇒ SupervisorStrategy.Resume 67 | } 68 | } 69 | sup ! Count 70 | (sup ? GetState).futureValue shouldBe CounterState(1L) 71 | sup ! Command(() ⇒ throw new RuntimeException("resume")) 72 | (sup ? GetState).futureValue shouldBe CounterState(1L) 73 | tp.expectNoMsg(100.millis) // no Terminated message 74 | cleanup(sup) 75 | } 76 | 77 | it should "restart the worker, so the worker instance has been replaced, and state should be 0 again" in { 78 | val tp = probe 79 | val sup = createSupervisor(tp) { 80 | OneForOneStrategy() { 81 | case t: RuntimeException ⇒ SupervisorStrategy.Restart 82 | } 83 | } 84 | sup ! Count 85 | (sup ? GetState).futureValue shouldBe CounterState(1L) 86 | sup ! Command(() ⇒ throw new RuntimeException("restart")) 87 | (sup ? GetState).futureValue shouldBe CounterState(0L) 88 | tp.expectNoMsg(100.millis) // no Terminated message 89 | cleanup(sup) 90 | } 91 | 92 | it should "stop the worker, so worker in not there anymore and should not answer" in { 93 | val tp = probe 94 | val sup = createSupervisor(tp) { 95 | OneForOneStrategy() { 96 | case t: RuntimeException ⇒ SupervisorStrategy.Stop 97 | } 98 | } 99 | sup ! Command(() ⇒ throw new RuntimeException("stop")) 100 | tp.expectMsgPF[Unit](100.millis) { 101 | case Terminated(_) ⇒ 102 | } 103 | cleanup(sup) 104 | } 105 | } 106 | --------------------------------------------------------------------------------