diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e2067d1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI +on: [push, pull_request] +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + java: [11, 16] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v1 + - uses: olafurpg/setup-scala@v11 + with: + java-version: "adopt@1.${{ matrix.java }}" + - uses: coursier/cache-action@v5 + - name: Test + run: | + #run: sbt clean coverage test + sbt test + rm -rf "$HOME/.ivy2/local" || true + find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true + find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.sbt -name "*.lock" -delete || true + upload_coverage: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: olafurpg/setup-scala@v11 + - run: sbt coverageReport codacyCoverage \ No newline at end of file diff --git a/.gitignore b/.gitignore index 666f2c0..80cf2a5 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,8 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties + +.bloop/ +.metals/ +.bsp/ +.vscode/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5eede68..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -dist: xenial - -jdk: - - oraclejdk8 - - oraclejdk11 - - openjdk8 - - openjdk11 - -language: scala -scala: - - 2.12.7 - - 2.13.0-M5 - -addons: - apt: - packages: - - bc - -script: - - sbt clean coverage test - -after_success: - - sbt coverageReport codacyCoverage \ No newline at end of file diff --git a/README.md b/README.md index f27e533..59f4d63 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Scala Expect [![license](http://img.shields.io/:license-MIT-blue.svg)](LICENSE) -[![Scaladoc](http://javadoc-badge.appspot.com/work.martins.simon/scala-expect_2.12.svg?label=scaladoc&style=plastic&maxAge=604800)](https://lasering.github.io/scala-expect/latest/api/work/martins/simon/expect/index.html) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/work.martins.simon/scala-expect_2.12/badge.svg?maxAge=604800)](https://maven-badges.herokuapp.com/maven-central/work.martins.simon/scala-expect_2.12) +[![Scaladoc](https://javadoc.io/badge2/work.martins.simon/scala-expect_3.1/javadoc.svg)](https://lasering.github.io/scala-expect/latest/api/work/martins/simon/expect/index.html) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/work.martins.simon/scala-expect_3.1/badge.svg?maxAge=604800)](https://maven-badges.herokuapp.com/maven-central/work.martins.simon/scala-expect_3.1) -[![Build Status](https://travis-ci.org/Lasering/scala-expect.svg?branch=master&style=plastic&maxAge=604800)](https://travis-ci.org/Lasering/scala-expect) -[![Codacy Badge](https://api.codacy.com/project/badge/coverage/74ba0150f4034c8294e66f6b97a2f69f)](https://www.codacy.com/app/IST-DSI/scala-expect) -[![Codacy Badge](https://api.codacy.com/project/badge/grade/74ba0150f4034c8294e66f6b97a2f69f)](https://www.codacy.com/app/IST-DSI/scala-expect) +[![example workflow](https://github.com/Lasering/scala-expect/actions/workflows/ci.yml/badge.svg)](https://github.com/Lasering/scala-expect/actions) +[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/156e74a155e64789a241ebb25c227598)](https://www.codacy.com/app/IST-DSI/scala-expect) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/156e74a155e64789a241ebb25c227598)](https://www.codacy.com/gh/Lasering/scala-expect/dashboard) [![BCH compliance](https://bettercodehub.com/edge/badge/Lasering/scala-expect)](https://bettercodehub.com/results/Lasering/scala-expect) A Scala implementation of a very small subset of the widely known TCL expect. @@ -17,14 +17,13 @@ libraryDependencies += "work.martins.simon" %% "scala-expect" % "6.0.0" ``` ## Core -#### [Documentation](../../wiki/Core) #### Advantages * Closer to metal / basis for the other flavors. * Immutable and therefore thread-safe. -* Most errors will be caught at compile time (eg. you won't be able to use a `SendWithRegex` inside a `StringWhen`). +* Most errors will be caught at compile time (eg. you won't be able to use a `Send` with regex inside a `When` matching strings). #### Disadvantages -* Verbose syntax. +* Pesky commas and parenthesis everywhere. * Can't cleanly add expect blocks/whens/actions based on a condition. #### Example @@ -34,35 +33,31 @@ import scala.concurrent.ExecutionContext.Implicits.global val e = new Expect("bc -i", defaultValue = 5)( ExpectBlock( - StringWhen("For details type `warranty'.")( - Sendln("1 + 2") - ) + When("For details type `warranty'.")( + Sendln("1 + 2"), + ), ), ExpectBlock( - RegexWhen("""\n(\d+)\n""".r)( - SendlnWithRegex { m => + When("""\n(\d+)\n""".r)( + Sendln { m => val previousAnswer = m.group(1) println(s"Got $previousAnswer") s"$previousAnswer + 3" - } - ) + }, + ), ), ExpectBlock( - RegexWhen("""\n(\d+)\n""".r)( - ReturningWithRegex(_.group(1).toInt) - ) - ) + When("""\n(\d+)\n""".r)( + Returning(_.group(1).toInt), + ), + ), ) e.run() //Returns 6 inside a Future[Int] ``` ## Fluent -#### [Documentation](../../wiki/Fluent) #### Advantages -* Less verbose syntax: - * StringWhen, RegexWhen, etc is just `when`. - * Returning, ReturningWithRegex, etc is just `returning`. - * Less commas and parenthesis. +* Fewer commas and parenthesis. * Most errors will be caught at compile time. * Easy to add expect blocks/whens/actions based on a condition. * Easy to refactor the creation of expects. @@ -71,7 +66,7 @@ e.run() //Returns 6 inside a Future[Int] #### Disadvantages * Some overhead since the fluent expect is just a builder for a core expect. * Mutable - the fluent expect has to maintain a state of the objects that have been built. -* IDE's will easily mess the custom indentation. +* Reformatting code in IDE's will mess the custom indentation. #### Example ```scala @@ -89,15 +84,14 @@ val e = new Expect("bc -i", defaultValue = 5) { println(s"Got $previousAnswer") s"$previousAnswer + 3" } - //This is a shortcut. It works just like the previous expect block. - expect("""\n(\d+)\n""".r) - .returning(_.group(1).toInt) + expect + .when("""\n(\d+)\n""".r) + .returning(_.group(1).toInt) } e.run() //Returns 6 inside a Future[Int] ``` ## DSL -#### [Documentation](../../wiki/DSL) #### Advantages * Code will be indented in blocks so IDE's won't mess the indentation. * Syntax more close to the TCL expect. @@ -105,8 +99,7 @@ e.run() //Returns 6 inside a Future[Int] * Easy to refactor the creation of expects. #### Disadvantages -* Most errors will only be caught at runtime as opposed to compile time. -* More overhead than the fluent expect since it's just a wrapper arround fluent expect. +* More overhead than the fluent expect since it's just a wrapper around fluent expect. * Mutable - it uses a fluent expect as the backing expect and a mutable stack to keep track of the current context. #### Example @@ -129,13 +122,14 @@ val e = new Expect("bc -i", defaultValue = 5) { } } } - //This is a shortcut. It works just like the previous expect block. - expect("""\n(\d+)\n""".r) { - returning(_.group(1).toInt) + expect { + when("""\n(\d+)\n""".r) { + returning(_.group(1).toInt) + } } } e.run() //Returns 6 inside a Future[Int] ``` ## License -Scala Expect is open source and available under the [MIT license](LICENSE). +Scala Expect is open source and available under the [MIT license](LICENSE). \ No newline at end of file diff --git a/build.sbt b/build.sbt index d19a6dd..e2165d5 100644 --- a/build.sbt +++ b/build.sbt @@ -5,66 +5,43 @@ name := "scala-expect" //==== Compile Options ================================================================================================= //====================================================================================================================== javacOptions ++= Seq("-Xlint", "-encoding", "UTF-8", "-Dfile.encoding=utf-8") -scalaVersion := "2.13.0-M5" -crossScalaVersions := Seq(scalaVersion.value, "2.12.8") - +scalaVersion := "3.1.1" scalacOptions ++= Seq( - "-deprecation", // Emit warning and location for usages of deprecated APIs. - "-encoding", "utf-8", // Specify character encoding used by source files. - "-explaintypes", // Explain type errors in more detail. - "-feature", // Emit warning and location for usages of features that should be imported explicitly. - "-language:implicitConversions", // Explicitly enables the implicit conversions feature - "-unchecked", // Enable additional warnings where generated code depends on assumptions. - "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. - "-Xfatal-warnings", // Fail the compilation if there are any warnings. - "-Xfuture", // Turn on future language features. - "-Xsource:2.14", // Treat compiler input as Scala source for the specified version. - "-Xmigration:2.14.0", // Warn about constructs whose behavior may have changed since version. - "-Xlint", // Enables every warning. scalac -Xlint:help for a list and explanation - "-Ywarn-dead-code", // Warn when dead code is identified. - "-Ywarn-numeric-widen", // Warn when numerics are widened. - "-Ywarn-value-discard", // Warn when non-Unit expression results are unused. - "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. - "-Ywarn-unused:imports", // Warn if an import selector is not referenced. - "-Ywarn-unused:privates", // Warn if a private member is unused. - "-Ywarn-unused:locals", // Warn if a local definition is unused. - "-Ywarn-unused:params", // Warn if a value parameter is unused. TODO this seams to not be working in 2.13 - "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused. - "-Ybackend-parallelism", "4", // Maximum worker threads for backend -) ++ (CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 12)) => Seq( - "-Xexperimental", // Enable experimental extensions. - "-Xsource:2.13", // Treat compiler input as Scala source for the specified version. - "-Xmigration:2.13.0", // Warn about constructs whose behavior may have changed since version. - "-Ypartial-unification", // Enable partial unification in type constructor inference - "-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. - ) - case _ => Nil -}) + //"-explain", // Explain errors in more detail. + //"-explain-types", // Explain type errors in more detail. + "-indent", // Allow significant indentation. + "-new-syntax", // Require `then` and `do` in control expressions. + "-feature", // Emit warning and location for usages of features that should be imported explicitly. + "-language:future", // better-monadic-for + "-language:implicitConversions", // Allow implicit conversions + "-deprecation", // Emit warning and location for usages of deprecated APIs. + "-Werror", // Fail the compilation if there are any warnings. + "-source:future", + "-Xsemanticdb", // Store information in SemanticDB. + "-Ycook-comments", // Cook the comments (type check `@usecase`, etc.) + //"-Ysafe-init", // Ensure safe initialization of objects + "-Yshow-suppressed-errors", // Also show follow-on errors and warnings that are normally suppressed. + // Compile code with classes specific to the given version of the Java platform available on the classpath and emit bytecode for this version. + //"-release", "16", + //"-project-url", git.remoteRepo.value, +) -// These lines ensure that in sbt console or sbt test:console the -Ywarn* and the -Xfatal-warning are not bothersome. -// https://stackoverflow.com/questions/26940253/in-sbt-how-do-you-override-scalacoptions-for-console-in-all-configurations -scalacOptions in (Compile, console) ~= (_ filterNot { option => - option.startsWith("-Ywarn") || option == "-Xfatal-warnings" -}) -scalacOptions in (Test, console) := (scalacOptions in (Compile, console)).value +Test / scalacOptions += "-Wconf:msg=is not declared `infix`:s,msg=is declared 'open':s" + +// These lines ensure that in sbt console or sbt test:console the -Werror is not bothersome. +Compile / console / scalacOptions ~= (_.filterNot(_.startsWith("-Werror"))) +Test / console / scalacOptions := (Compile / console / scalacOptions).value //====================================================================================================================== //==== Dependencies ==================================================================================================== //====================================================================================================================== -val silencerVersion = "1.3.0" libraryDependencies ++= Seq( - "com.typesafe" % "config" % "1.3.3", - "com.zaxxer" % "nuprocess" % "1.2.4", - "ch.qos.logback" % "logback-classic" % "1.2.3" % Test, - "org.scalatest" %% "scalatest" % "3.0.6-SNAP5" % Test, - compilerPlugin("com.github.ghik" %% "silencer-plugin" % silencerVersion), - "com.github.ghik" %% "silencer-lib" % silencerVersion % Compile -) ++ (CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 13)) => Seq("com.typesafe.scala-logging" %% "scala-logging" % "3.9.1") - case Some((2, 12)) => Seq("com.typesafe.scala-logging" %% "scala-logging" % "3.9.0") - case _ => Nil -}) + "com.typesafe" % "config" % "1.4.1", + "com.zaxxer" % "nuprocess" % "2.0.2", + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", + "ch.qos.logback" % "logback-classic" % "1.2.10" % Test, + "org.scalatest" %% "scalatest" % "3.2.11" % Test, +) // Needed for scoverage snapshot resolvers += Opts.resolver.sonatypeSnapshots @@ -77,22 +54,22 @@ git.useGitDescribe := true latestReleasedVersion := git.gitDescribedVersion.value.getOrElse("0.1.0") autoAPIMappings := true //Tell scaladoc to look for API documentation of managed dependencies in their metadata. -scalacOptions in (Compile, doc) ++= Seq( +Compile / doc / scalacOptions ++= Seq( "-diagrams", // Create inheritance diagrams for classes, traits and packages. "-groups", // Group similar functions together (based on the @group annotation) "-implicits", // Document members inherited by implicit conversions. "-doc-title", name.value.capitalize, "-doc-version", latestReleasedVersion.value, "-doc-source-url", s"${homepage.value.get}/tree/v${latestReleasedVersion.value}€{FILE_PATH}.scala", - "-sourcepath", (baseDirectory in ThisBuild).value.getAbsolutePath + "-sourcepath", baseDirectory.value.getAbsolutePath ) //Define the base URL for the Scaladocs for your library. This will enable clients of your library to automatically //link against the API documentation using autoAPIMappings. apiURL := Some(url(s"${homepage.value.get}/${latestReleasedVersion.value}/api/")) enablePlugins(GhpagesPlugin) -siteSubdirName in SiteScaladoc := s"api/${version.value}" -envVars in ghpagesPushSite := Map("SBT_GHPAGES_COMMIT_MESSAGE" -> s"Add Scaladocs for version ${latestReleasedVersion.value}") +SiteScaladoc / siteSubdirName := s"api/${version.value}" +ghpagesPushSite / envVars := Map("SBT_GHPAGES_COMMIT_MESSAGE" -> s"Add Scaladocs for version ${latestReleasedVersion.value}") git.remoteRepo := s"git@github.com:Lasering/${name.value}.git" //====================================================================================================================== @@ -111,13 +88,12 @@ developers += Developer("Lasering", "Simão Martins", "", url("https://github.co dependencyUpdatesFailBuild := true coverageFailOnMinimum := true -coverageMinimum := 90 +coverageMinimumStmtTotal := 90 +coverageMinimumBranchTotal := 90 -releaseCrossBuild := true releasePublishArtifactsAction := PgpKeys.publishSigned.value import ReleaseTransformations._ -import xerial.sbt.Sonatype.SonatypeCommand releaseProcess := Seq[ReleaseStep]( releaseStepTask(dependencyUpdates), checkSnapshotDependencies, @@ -129,7 +105,7 @@ releaseProcess := Seq[ReleaseStep]( tagRelease, releaseStepTask(ghpagesPushSite), publishArtifacts, - releaseStepCommand(SonatypeCommand.sonatypeReleaseAll), + releaseStepCommand("sonatypeReleaseAll"), pushChanges, setNextVersion ) \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index cabf73b..b46cfa1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.2.7 +sbt.version = 1.6.2 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 06c8039..ea3322d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,11 +1,7 @@ -logLevel := Level.Warn - -addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.9") -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.4") -addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.2") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0-M5") -addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "2.112") - -addCompilerPlugin("io.tryp" % "splain" % "0.3.4" cross CrossVersion.patch) \ No newline at end of file +addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.13") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.1") +addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.1.1") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.10") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.0-M4") +addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "3.0.3") \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/EndOfFile.scala b/src/main/scala/work/martins/simon/expect/EndOfFile.scala deleted file mode 100644 index 6c2014d..0000000 --- a/src/main/scala/work/martins/simon/expect/EndOfFile.scala +++ /dev/null @@ -1,3 +0,0 @@ -package work.martins.simon.expect - -object EndOfFile diff --git a/src/main/scala/work/martins/simon/expect/FromInputStream.scala b/src/main/scala/work/martins/simon/expect/FromInputStream.scala deleted file mode 100644 index 8cb37a7..0000000 --- a/src/main/scala/work/martins/simon/expect/FromInputStream.scala +++ /dev/null @@ -1,5 +0,0 @@ -package work.martins.simon.expect - -sealed trait FromInputStream -case object StdOut extends FromInputStream -case object StdErr extends FromInputStream diff --git a/src/main/scala/work/martins/simon/expect/Settings.scala b/src/main/scala/work/martins/simon/expect/Settings.scala index 59d787d..9393e83 100644 --- a/src/main/scala/work/martins/simon/expect/Settings.scala +++ b/src/main/scala/work/martins/simon/expect/Settings.scala @@ -2,31 +2,27 @@ package work.martins.simon.expect import java.nio.charset.{Charset, StandardCharsets} import java.util.concurrent.TimeUnit - import com.typesafe.config.{Config, ConfigFactory} - import scala.concurrent.duration.{DurationLong, FiniteDuration} -object Settings { +object Settings: /** * Instantiate a `Settings` from a Typesafe Config. * @param config The Config from which to parse the settings. */ - def fromConfig(config: Config = ConfigFactory.load()): Settings = { - val scalaExpectConfig: Config = { + def fromConfig(config: Config = ConfigFactory.load()): Settings = + val scalaExpectConfig: Config = val reference = ConfigFactory.defaultReference() val finalConfig = config.withFallback(reference) finalConfig.checkValid(reference, "scala-expect") finalConfig.getConfig("scala-expect") - } - import scalaExpectConfig._ - + + import scalaExpectConfig.* + Settings( getDuration("timeout", TimeUnit.SECONDS).seconds, getDouble("time-factor"), Charset.forName(getString("charset"))) - } -} /** * This class holds all the settings that parameterize expect. @@ -37,12 +33,11 @@ object Settings { * @param timeoutFactor Factor by which to scale timeout, e.g. to account for shared build system load. * @param charset The charset used for encoding and decoding the read text and the to be printed text. */ -case class Settings(timeout: FiniteDuration = 1.second, timeoutFactor: Double = 1.0, charset: Charset = StandardCharsets.UTF_8) { - require(timeoutFactor >= 1.0 && timeoutFactor < Long.MaxValue && !timeoutFactor.isInfinite && !timeoutFactor.isNaN, - "Time factor must be >=1, < Long.MaxValue, and not Infinity or NaN.") - +case class Settings(timeout: FiniteDuration = 1.second, timeoutFactor: Double = 1.0, charset: Charset = StandardCharsets.UTF_8) derives CanEqual: + require(timeoutFactor >= 1.0 && !timeoutFactor.isInfinite && !timeoutFactor.isNaN, + "Time factor must be >=1 and not Infinity or NaN.") + /** The timeout scaled by the timeout factor.*/ val scaledTimeout: FiniteDuration = (timeout * timeoutFactor).asInstanceOf[FiniteDuration] -} // The default values declared here are the authoritative ones. But its nice to have the same default values in reference.conf. // If you change any of them make sure they are the same. \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/StringUtils.scala b/src/main/scala/work/martins/simon/expect/StringUtils.scala index 6dd4bbb..dbbb05e 100644 --- a/src/main/scala/work/martins/simon/expect/StringUtils.scala +++ b/src/main/scala/work/martins/simon/expect/StringUtils.scala @@ -1,16 +1,19 @@ package work.martins.simon.expect -object StringUtils { - //http://stackoverflow.com/questions/9913971/scala-how-can-i-get-an-escaped-representation-of-a-string - def escape(raw: String): String = { - import scala.reflect.runtime.universe._ - Literal(Constant(raw)).toString - } +import scala.quoted.* - def splitBySpaces(command: String): Seq[String] = command.split("""\s+""").filter(_.nonEmpty).toSeq - - implicit class IndentableString(s: String) { +object StringUtils: + inline def escape(raw: String): String = raw + /*inline def escape(inline raw: String): String = ${escapeImpl('{raw})} + + def escapeImpl(raw: Expr[String])(using Quotes): Expr[String] = + import quotes.reflect.* + Literal(StringConstant(raw.show)).asExprOf[String]*/ + + def properCommand(command: Seq[String] | String): Seq[String] = command match + case s: String => s.split("""\s+""").filter(_.nonEmpty).toSeq + case seq: Seq[String] => seq + + implicit class IndentableString(s: String): def indent(level: Int = 1, text: String = "\t"): String = s.replaceAll("(?m)^", text * level) - //def unindent(level: Int = 1, text: String = "\t"): String = s.replaceAll(s"(?m)^${text * level}", "") - } -} + //def unindent(level: Int = 1, text: String = "\t"): String = s.replaceAll(s"(?m)^${text * level}", "") \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/Timeout.scala b/src/main/scala/work/martins/simon/expect/Timeout.scala deleted file mode 100644 index e4cf51f..0000000 --- a/src/main/scala/work/martins/simon/expect/Timeout.scala +++ /dev/null @@ -1,3 +0,0 @@ -package work.martins.simon.expect - -object Timeout diff --git a/src/main/scala/work/martins/simon/expect/core/Expect.scala b/src/main/scala/work/martins/simon/expect/core/Expect.scala index bd03567..51729f1 100644 --- a/src/main/scala/work/martins/simon/expect/core/Expect.scala +++ b/src/main/scala/work/martins/simon/expect/core/Expect.scala @@ -1,12 +1,11 @@ package work.martins.simon.expect.core -import scala.concurrent._ +import scala.concurrent.* import scala.util.{Failure, Success} - import com.typesafe.scalalogging.LazyLogging -import work.martins.simon.expect.Settings -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect.core.RunContext.{ChangeToNewExpect, Continue, Terminate} +import work.martins.simon.expect.{/=>, Settings} +import work.martins.simon.expect.StringUtils.* +import work.martins.simon.expect.core.ExecutionAction.* /** * Expect allows you to invoke CLIs and ensure they return what you expect. @@ -53,196 +52,196 @@ import work.martins.simon.expect.core.RunContext.{ChangeToNewExpect, Continue, T * Finally the last interaction is very similar to the second one, with the only difference being the matched digit is * not used to send more text to `bc` but rather to set the value the expect will return in the end of the execution. * - * If you find the syntax used to create the $type too verbose you should look at [[work.martins.simon.expect.fluent.Expect]] + * If you find the syntax used to create the Expect too verbose you should look at [[work.martins.simon.expect.fluent.Expect]] * or [[work.martins.simon.expect.dsl.Expect]] which provide a more terse syntax, especially `dsl.Expect` which provides * a syntax very similar to Tcl Expect. * - * @param command the command this $type will execute. + * @param command the command this Expect will execute. * @param defaultValue the value that will be returned if no `Returning` action is executed. - * @param settings the settings that parameterize the execution of this $type. - * @param expectBlocks the `ExpectBlock`s that describe the interactions this $type will execute. - * @tparam R the type this $type returns. - * - * @define type `Expect` + * @param settings the settings that parameterize the execution of this Expect. + * @param expectBlocks the `ExpectBlock`s that describe the interactions this Expect will execute. + * @tparam R the type this Expect returns. */ -final case class Expect[+R](command: Seq[String], defaultValue: R, settings: Settings = Settings.fromConfig()) - (val expectBlocks: ExpectBlock[R]*) extends LazyLogging { - def this(command: String, defaultValue: R, settings: Settings)(expectBlocks: ExpectBlock[R]*) = { - this(splitBySpaces(command), defaultValue, settings)(expectBlocks:_*) - } - - def this(command: String, defaultValue: R)(expectBlocks: ExpectBlock[R]*) = { - this(command, defaultValue, Settings.fromConfig())(expectBlocks:_*) - } - - require(command.nonEmpty, "Expect must have a command to run.") - +final case class Expect[+R](command: Seq[String] | String, defaultValue: R, settings: Settings = Settings.fromConfig()) + (val expectBlocks: ExpectBlock[R]*) extends LazyLogging derives CanEqual: + val commandSeq: Seq[String] = properCommand(command) + require(commandSeq.nonEmpty, "Expect must have a command to run.") + /** * Runs this Expect using the given `settings`. Or in other words overrides the Settings set at construction time. - * + * * In order to uniformly scale the timeouts just change `settings.timeFactor` to a value bigger than 1. - * + * * @param propagateSettings whether to use the given `settings` when executing the Expects returned by this Expect. * For example if set to true and this Expect contains `ReturningExpect` then the returned - * Expect will be executed using `settings` instead of the Settings with which it has created. + * Expect will be executed using `settings` instead of the Settings with which it has created. * @param ex * @return */ - def run(settings: Settings = this.settings, propagateSettings: Boolean = false)(implicit ex: ExecutionContext): Future[R] = { - run(NuProcessRichProcess(command, settings), propagateSettings) + def run(settings: Settings = this.settings, propagateSettings: Boolean = false)(using ExecutionContext): Future[R] = + run(NuProcessRichProcess(commandSeq, settings), propagateSettings) + + private def success(runContext: RunContext[R])(using ex: ExecutionContext): Future[R] = Future { + runContext.process.destroy() + }.map { _ => + logger.info(runContext.withId(s"Finished returning: ${runContext.value}")) + runContext.value } - def run(process: RichProcess, propagateSettings: Boolean)(implicit ex: ExecutionContext): Future[R] = { + + /** + * Runs this Expect using the specified `process`. + * This is the default interpreter using Future. + * @param process a Process capable of reading from StdOut and StdErr, as well as writing to StdOut. + * @param propagateSettings whether nested Expects should use the same settings as this Expect. + * @return a Future with the returned result. If no `Returning` action is specified the `defaultValue` will be used. + */ + def run(process: RichProcess, propagateSettings: Boolean)(using ExecutionContext): Future[R] = val runContext = RunContext(process, value = defaultValue, executionAction = Continue) - logger.info(runContext.withId("Running command: " + command.mkString("\"", " ", "\""))) + logger.info(runContext.withId("Running command: " + commandSeq.mkString("\"", " ", "\""))) logger.debug(runContext.withId(runContext.settings.toString)) - - def success(runContext: RunContext[R]): Future[R] = Future { - runContext.process.destroy() - } map { _ => - logger.info(runContext.withId(s"Finished returning: ${runContext.value}")) - runContext.value - } - - def innerRun(expectBlocks: Seq[ExpectBlock[R]], runContext: RunContext[R]): Future[R] = { + + def innerRun(expectBlocks: Seq[ExpectBlock[R]], runContext: RunContext[R]): Future[R] = + // TODO: how to implement this in a tail recursive way? expectBlocks.headOption.map { headExpectBlock => //We still have expect blocks to run val result = headExpectBlock.run(runContext).flatMap { innerRunContext => - innerRunContext.executionAction match { - case Continue => - //Continue with the remaining expect blocks - innerRun(expectBlocks.tail, innerRunContext) - case Terminate => - success(innerRunContext) + innerRunContext.executionAction match + case Continue => innerRun(expectBlocks.tail, innerRunContext) + case Terminate => success(innerRunContext) case ChangeToNewExpect(newExpect) => innerRunContext.process.destroy() - if (propagateSettings) { - newExpect.asInstanceOf[Expect[R]].run(runContext.settings) - } else { - newExpect.asInstanceOf[Expect[R]].run() - } - } + // How does this cast work? + newExpect.run(if propagateSettings then runContext.settings else newExpect.settings).asInstanceOf[Future[R]] } //If we get an exception while running the head expect block we want to make sure the rich process is destroyed. result.transformWith { case Success(r) => Future.successful(r) // The success function already destroys the process case Failure(t) => Future { runContext.process.destroy() }.map(_ => throw t) } - } getOrElse { - //No more expect blocks. Just return the success value. - success(runContext) - } - } - + }.getOrElse(success(runContext)) + innerRun(expectBlocks, runContext) - } - - /** Creates a new $type by applying a function to this $type result. - * - * @tparam T the type of the returned $type - * @param f the function which will be applied to this $type result - * @return an $type which will return the result of the application of the function `f` + + /** + * Creates a new Expect by applying a function to this Expect result. + * + * @tparam T the type of the returned Expect + * @param f the function which will be applied to this Expect result + * @return an Expect which will return the result of the application of the function `f` * @group Transformations */ - def map[T](f: R => T): Expect[T] = { - Expect(command, f(defaultValue), settings)(expectBlocks.map(_.map(f)):_*) - } - /** Creates a new $type by applying a function to this $type result, and returns the result of the function as the new $type. + def map[T](f: R => T): Expect[T] = + Expect(command, f(defaultValue), settings)(expectBlocks.map(_.map(f))*) + + /** + * Creates a new Expect by applying a function to this Expect result, and returns the result of the function as the new Expect. * - * @tparam T the type of the returned $type - * @param f the function which will be applied to this $type result - * @return the $type returned as the result of the application of the function `f` + * @tparam T the type of the returned Expect + * @param f the function which will be applied to this Expect result + * @return the Expect returned as the result of the application of the function `f` * @group Transformations */ - def flatMap[T](f: R => Expect[T]): Expect[T] = { - Expect(command, f(defaultValue).defaultValue, settings)(expectBlocks.map(_.flatMap(f)):_*) - } - /** Creates a new $type with one level of nesting flattened, this method is equivalent to `flatMap(identity)`. - * @tparam T the type of the returned $type - * @return an $type with one level of nesting flattened + def flatMap[T](f: R => Expect[T]): Expect[T] = + Expect(command, f(defaultValue).defaultValue, settings)(expectBlocks.map(_.flatMap(f))*) + + /** + * Creates a new Expect with one level of nesting flattened, this method is equivalent to `flatMap(identity)`. + * @tparam T the type of the returned Expect + * @return an Expect with one level of nesting flattened * @group Transformations */ - def flatten[T](implicit ev: R <:< Expect[T]): Expect[T] = flatMap(ev) - private def notDefined[RR >: R](function: String, text: String)(result: RR) = + def flatten[T](using ev: R <:< Expect[T]): Expect[T] = flatMap(ev) + + private def notDefined(function: String, text: String)(result: R): Nothing = throw new NoSuchElementException(s"""Expect.$function: $text "$result"""") - /** Creates a new $type by filtering its result with a predicate. + + /** + * Creates a new Expect by filtering its result with a predicate. * - * If the current $type result satisfies the predicate, the new $type will also hold that result. - * Otherwise, the resulting $type will fail with a `NoSuchElementException`. + * If the current Expect result satisfies the predicate, the new Expect will also hold that result. + * Otherwise, the resulting Expect will fail with a `NoSuchElementException`. * - * @param p the predicate to apply to the result of this $type + * @param p the predicate to apply to the result of this Expect * @group Transformations */ def filter(p: R => Boolean): Expect[R] = map { r => - if (p(r)) r else notDefined("filter", "predicate is not satisfied for")(r) + if p(r) then r else notDefined("filter", "predicate is not satisfied for")(r) } - /** Used by for-comprehensions. + + /** + * Used by for-comprehensions. + * Expect is already lazy this is just an alias for filter. * @group Transformations */ - def withFilter(p: R => Boolean): Expect[R] = filter(p) // Expect is already lazy, so we can simply call filter. - /** Creates a new $type by mapping the result of the current $type, if the given partial function is defined at that value. + def withFilter(p: R => Boolean): Expect[R] = filter(p) // + + /** + * Creates a new Expect by mapping the result of the current Expect, if the given partial function is defined at that value. * - * If the current $type contains a value for which the partial function is defined, the new $type will also hold that value. - * Otherwise, the resulting $type will fail with a `NoSuchElementException`. + * If the current Expect contains a value for which the partial function is defined, the new Expect will also hold that value. + * Otherwise, the resulting Expect will fail with a `NoSuchElementException`. * - * @tparam T the type of the returned $type - * @param pf the `PartialFunction` to apply to the result of this $type - * @return an $type holding the result of application of the `PartialFunction` or a `NoSuchElementException` + * @tparam T the type of the returned Expect + * @param pf the `PartialFunction` to apply to the result of this Expect + * @return an Expect holding the result of application of the `PartialFunction` or a `NoSuchElementException` * @group Transformations */ def collect[T](pf: PartialFunction[R, T]): Expect[T] = map { r => pf.applyOrElse(r, notDefined("collect", "partial function is not defined at")) } - /** Creates a new $type by flatMapping the result of the current $type, if the given partial function is defined at that value. + + /** + * Creates a new Expect by flatMapping the result of the current Expect, if the given partial function is defined at that value. * - * If the current $type contains a value for which the partial function is defined, the new $type will also hold that value. - * Otherwise, the resulting $type will fail with a `NoSuchElementException`. + * If the current Expect contains a value for which the partial function is defined, the new Expect will also hold that value. + * Otherwise, the resulting Expect will fail with a `NoSuchElementException`. * - * @tparam T the type of the returned $type - * @param pf the `PartialFunction` to apply to the result of this $type - * @return an $type holding the result of application of the `PartialFunction` or a `NoSuchElementException` + * @tparam T the type of the returned Expect + * @param pf the `PartialFunction` to apply to the result of this Expect + * @return an Expect holding the result of application of the `PartialFunction` or a `NoSuchElementException` * @group Transformations */ def flatCollect[T](pf: PartialFunction[R, Expect[T]]): Expect[T] = flatMap { r => pf.applyOrElse(r, notDefined("flatCollect", "partial function is not defined at")) } + // TODO improve the example - /** Transform this $type result using the following strategy: + /** + * Transform this Expect result using the following strategy: * - if `flatMapPF` is defined for this expect result then flatMap the result using flatMapPF. * - otherwise, if `mapPF` is defined for this expect result then map the result using mapPF. * - otherwise a NoSuchElementException is thrown where the result would be expected. * - * This function is very useful when you need to flatMap this $type for some values of its result type and map - * this $type for some other values of its result type. In other words it `collect`s and `flatCollect`s this $type simultaneously. + * This function is very useful when you need to flatMap this Expect for some values of its result type and map + * this Expect for some other values of its result type. In other words it `collect`s and `flatCollect`s this Expect simultaneously. * * To ensure you don't get NoSuchElementException you should take special care in ensuring: * {{{ domain(flatMapPF) ∪ domain(mapPF) == domain(R) }}} * Remember that in the domain of R the `defaultValue` is also included. * * @example {{{ - * def countFilesInFolder(folder: String): Expect[Either[String, Int]] = { - * val e = new Expect(s"ls -1 $$folder", Left("unknownError"): Either[String, Int])( + * def countFilesInFolder(folder: String): Expect[Either[String, Int]] = + * new Expect(s"ls -1 $$folder", Left("unknownError"): Either[String, Int])( * ExpectBlock( - * StringWhen("access denied")( + * When("access denied")( * Returning(Left("Access denied")) * ), - * RegexWhen("(?s)(.*)".r)( - * ReturningWithRegex(_.group(1).split("\n").length) + * When("(?s)(.*)".r)( + * Returning(_.group(1).split("\n").length) * ) * ) * ) - * e - * } * - * def ensureFolderIsEmpty(folder: String): Expect[Either[String, Unit]] = { + * def ensureFolderIsEmpty(folder: String): Expect[Either[String, Unit]] = * countFilesInFolder(folder).transform({ * case Right(numberOfFiles) if numberOfFiles > 0 => * Expect(s"rm -r $$folder", Left("unknownError"): Either[String, Unit])( * ExpectBlock( - * StringWhen("access denied")( + * String("access denied")( * Returning(Left("Access denied")) * ), - * EndOfFileWhen( + * When(EndOfFile)( * Returning(()) * ) * ) @@ -251,90 +250,86 @@ final case class Expect[+R](command: Seq[String], defaultValue: R, settings: Set * case Left(l) => Left(l) // Propagate the error * case Right(0) => Right(()) * }) - * } * }}} - * @tparam T the type of the returned $type + * @tparam T the type of the returned Expect * @param flatMapPF the function that will be applied when a flatMap is needed * @param mapPF the function that will be applied when a map is needed - * @return a new $type whose result is either flatMapped or mapped according to whether flatMapPF or + * @return a new Expect whose result is either flatMapped or mapped according to whether flatMapPF or * mapPF is defined for the given result * @group Transformations */ - def transform[T](flatMapPF: PartialFunction[R, Expect[T]], mapPF: PartialFunction[R, T]): Expect[T] = { + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): Expect[T] = val newDefaultValue = flatMapPF.andThen(_.defaultValue).orElse(mapPF) .applyOrElse(defaultValue, notDefined("transform", "neither flatMapPF nor mapPF are defined at the Expect default value")) - - new Expect[T](command, newDefaultValue, settings)(expectBlocks.map(_.transform(flatMapPF, mapPF)):_*) - } - /** Zips the results of `this` and `that` $type, and creates a new $type holding the tuple of their results. + + new Expect[T](command, newDefaultValue, settings)(expectBlocks.map(_.transform(flatMapPF, mapPF))*) + + /** + * Zips the results of `this` and `that` Expect, and creates a new Expect holding the tuple of their results. * - * @tparam T the type of the returned $type - * @param that the other $type - * @return an $type with the results of both ${type}s + * @tparam T the type of the returned Expect + * @param that the other Expect + * @return an Expect with the results of both Expects * @group Transformations */ def zip[T](that: Expect[T]): Expect[(R, T)] = zipWith(that)((r1, r2) => (r1, r2)) - /** Zips the results of `this` and `that` $type using a function `f`, - * and creates a new $type holding the result. + + /** + * Zips the results of `this` and `that` Expect using a function `f`, + * and creates a new Expect holding the result. * - * @tparam T the type of the other $type - * @tparam U the type of the resulting $type - * @param that the other $type + * @tparam T the type of the other Expect + * @tparam U the type of the resulting Expect + * @param that the other Expect * @param f the function to apply to the results of `this` and `that` - * @return an $type with the result of the application of `f` to the results of `this` and `that` + * @return an Expect with the result of the application of `f` to the results of `this` and `that` * @group Transformations */ def zipWith[T, U](that: Expect[T])(f: (R, T) => U): Expect[U] = flatMap(r1 => that.map(r2 => f(r1, r2))) - + override def toString: String = s"""Expect: |\tCommand: $command |\tDefaultValue: $defaultValue |\tSettings: $settings - |${expectBlocks.mkString("\n").indent()} - """.stripMargin - + |${expectBlocks.mkString("\n").indent()}""".stripMargin + /** - * Returns whether `other` is an $type with the same `command`, the same `defaultValue`, the same `settings` and - * the same `expectBlocks` as this $type. + * Returns whether `other` Expect has the same: + * - command + * - defaultValue + * - settings + * - expectBlocks + * as this Expect. * * If any `expectBlock` contains an Action with a function, eg. Returning, this method will return false, * because equality is not defined for functions. * * The method `structurallyEqual` can be used to test whether two expects have the same structure. * - * @param other the other $type to compare this $type to. + * @param other the other Expect to compare this Expect to. */ - override def equals(other: Any): Boolean = other match { - case that: Expect[R] => - command == that.command && - defaultValue == that.defaultValue && - settings == that.settings && - expectBlocks == that.expectBlocks + override def equals(obj: Any): Boolean = obj.asInstanceOf[Matchable] match + case e @ Expect(`command`, `defaultValue`, `settings`) if e.expectBlocks == expectBlocks => true case _ => false - } - + /** - * @define subtypes expect blocks - * Returns whether the other $type has the same + * Returns whether the `other` Expect has structurally the same: * - command * - defaultvalue * - settings - * - number of $subtypes and that each pair of $subtypes is structurally equal as this $type. - * - * @param other the other $type to compare this $type to. + * - expectBlocks (compared structurally) + * as this Expect. + * @param other the other Expect to compare this Expect to. */ - def structurallyEquals[RR >: R](other: Expect[RR]): Boolean = { - command == other.command && + def structurallyEquals[RR >: R](other: Expect[RR]): Boolean = + commandSeq == other.commandSeq && defaultValue == other.defaultValue && settings == other.settings && expectBlocks.size == other.expectBlocks.size && expectBlocks.zip(other.expectBlocks).forall{ case (a, b) => a.structurallyEquals(b) } - } - - override def hashCode(): Int = { + + override def hashCode(): Int = Seq(command, defaultValue, settings, expectBlocks) .map(_.hashCode()) - .foldLeft(0)((a, b) => 31 * a + b) - } -} \ No newline at end of file + .foldLeft(0)((a, b) => 31 * a + b) \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/ExpectBlock.scala b/src/main/scala/work/martins/simon/expect/core/ExpectBlock.scala index a083165..21df8b6 100644 --- a/src/main/scala/work/martins/simon/expect/core/ExpectBlock.scala +++ b/src/main/scala/work/martins/simon/expect/core/ExpectBlock.scala @@ -1,69 +1,60 @@ package work.martins.simon.expect.core import java.io.EOFException - -import com.typesafe.scalalogging.LazyLogging -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect.{StdErr, StdOut} - import scala.concurrent.duration.Deadline import scala.concurrent.{ExecutionContext, Future, TimeoutException} +import com.typesafe.scalalogging.LazyLogging +import work.martins.simon.expect.{/=>, FromInputStream} +import work.martins.simon.expect.StringUtils.* +import work.martins.simon.expect.FromInputStream.* +import work.martins.simon.expect.core.ExecutionAction.* -/** - * @define type ExpectBlock - */ -final case class ExpectBlock[+R](whens: When[R]*) extends LazyLogging { +final case class ExpectBlock[+R](whens: When[R]*) extends LazyLogging: require(whens.nonEmpty, "ExpectBlock must have at least a When.") - private def timeoutWhen[RR >: R](runContext: RunContext[RR])(implicit deadline: Deadline): (When[RR], RunContext[RR]) = { + private def timeoutWhen[RR >: R](runContext: RunContext[RR])(using deadline: Deadline): (When[RR], RunContext[RR]) = logger.info(runContext.withId(s"Read timed out after ${deadline.time}.")) - whens.collectFirst { case when @ TimeoutWhen() => (when, runContext) } - .getOrElse(throw new TimeoutException) - } - - private def read[RR >: R](runContext: RunContext[RR])(implicit deadline: Deadline): RunContext[RR] = { - if (runContext.output.nonEmpty) { - logger.debug(runContext.withId(s"Did not match with last ${runContext.readFrom} output. Going to read more.")) - } - val readText = runContext.process.read(runContext.readFrom) - val newRunContext = runContext.withOutput(_ + readText) - logger.info(runContext.withId(s"Newly read text from ${runContext.readFrom}:\n$readText")) - logger.debug(runContext.withId(s"New ${runContext.readFrom} output:\n${newRunContext.output}")) - newRunContext - } - - private def obtainMatchingWhen[RR >: R](runContext: RunContext[RR])(readFunction: RunContext[RR] => RunContext[RR]) - (implicit ex: ExecutionContext, deadline: Deadline): Future[(When[RR], RunContext[RR])] = { + whens.collectFirst { + case when: TimeoutWhen[R] => (when, runContext) + }.getOrElse(throw new TimeoutException) + + private def endOfFileWhen[RR >: R](runContext: RunContext[RR])(using deadline: Deadline): (When[RR], RunContext[RR]) = + logger.info(runContext.withId(s"Read returned EndOfFile.")) + whens.collectFirst { + case when @ EndOfFileWhen(runContext.readFrom, _) => (when, runContext) + }.getOrElse(throw new EOFException) + + private def obtainMatchingWhen[RR >: R](runContext: RunContext[RR])(read: RunContext[RR] => (FromInputStream, String)) + (using ex: ExecutionContext, deadline: Deadline): Future[(When[RR], RunContext[RR])] = // Try to match against the output we have so far - whens.find(w => w.readFrom == runContext.readFrom && w.matches(runContext.output)) match { + whens.find(w => w.readFrom == runContext.readFrom && w.matches(runContext.output)) match case Some(when) => // The existing output was enough to get a matching when Future.successful((when, runContext)) case None if deadline.hasTimeLeft() => // We need more output and since we still have time left we are going to try and read more output. Future { - readFunction(runContext) + if runContext.output.nonEmpty then logger.debug(runContext.withId(s"Did not match with last ${runContext.readFrom} output.")) + logger.trace(runContext.withId(s"Time left in deadline ${deadline.timeLeft}")) + val (from, text) = read(runContext) + val newRunContext = runContext.readingFrom(from).withOutput(_ + text) + logger.info(runContext.withId(s"Newly read text from $from:\n$text")) + logger.debug(runContext.withId(s"New $from output:\n${newRunContext.output}")) + newRunContext } flatMap { newRunContext => - obtainMatchingWhen(newRunContext)(readFunction) + obtainMatchingWhen(newRunContext)(read) } recover { - case e: EOFException => - logger.info(runContext.withId(s"Read returned EndOfFile.")) - whens.collectFirst { - case when @ EndOfFileWhen(runContext.readFrom) => (when, runContext) - }.getOrElse(throw e) - case _: TimeoutException => - timeoutWhen(runContext) + case _: EOFException => endOfFileWhen(runContext) + case _: TimeoutException => timeoutWhen(runContext) } case _ => // We have no match nor time to read more output, so we just timeout. Future(timeoutWhen(runContext)) - } - } - + /** - * First checks if any of the Whens of this $type matches against the last output. + * First checks if any of the Whens of this ExpectBlock matches against the last output. * If one such When exists then the result of executing it is returned. - * Otherwise continuously reads text from `process` until one of the Whens of this $type matches against it. + * Otherwise continuously reads text from `process` until one of the Whens of this ExpectBlock matches against it. * If it is not able to do so before the timeout expires a TimeoutException will be thrown inside the Future. * * @param runContext the current run context of this expect execution. @@ -71,64 +62,47 @@ final case class ExpectBlock[+R](whens: When[R]*) extends LazyLogging { * @return the result of executing the When that matches either `lastOutput` or the text read from `process`. * Or a TimeoutException. */ - private[core] def run[RR >: R](runContext: RunContext[RR])(implicit ex: ExecutionContext): Future[RunContext[RR]] = { - implicit val deadline: Deadline = runContext.settings.scaledTimeout.fromNow - - val matchingWhen = whens.map(_.readFrom).distinct match { + private[core] def run[RR >: R](runContext: RunContext[RR])(using ex: ExecutionContext): Future[RunContext[RR]] = + given Deadline = runContext.settings.scaledTimeout.fromNow + + val matchingWhen = whens.map(_.readFrom).distinct match case Seq(from) => logger.debug(runContext.withId(s"Now running (reading from $from):\n$this")) - obtainMatchingWhen(runContext.readingFrom(from))(read) + obtainMatchingWhen(runContext.readingFrom(from)) { innerContext => + logger.debug(innerContext.withId(s"Going to read more from $from.")) + (from, innerContext.process.read(from)) + } case _ => logger.debug(runContext.withId(s"Now running (reading from StdOut and StdErr concurrently):\n$this")) logger.debug(runContext.withId("Going to try and match with last StdErr output.")) - whens.find(w => w.matches(runContext.stdErrOutput) && w.readFrom == StdErr) match { + whens.find(w => w.matches(runContext.stdErrOutput) && w.readFrom == StdErr) match case Some(when) => Future.successful((when, runContext)) case None => - logger.debug(runContext.withId("Did not match with last StdErr output. Going to try and match with last StdOut output.")) + if runContext.stdErrOutput.nonEmpty then logger.debug(runContext.withId("Did not match with last StdErr output.")) + logger.debug(runContext.withId("Going to try and match with last StdOut output.")) obtainMatchingWhen(runContext.readingFrom(StdOut)) { innerContext => - logger.debug(innerContext.withId(s"Did not match with last ${innerContext.readFrom} output." + - s" Going to concurrently read from StdOut and StdErr.")) - logger.trace(innerContext.withId(s"Time left in deadline ${deadline.timeLeft}")) - - val (from, text) = innerContext.process.readOnFirstInputStream() - val newRunContext = innerContext.readingFrom(from).withOutput(_ + text) - logger.info(innerContext.withId(s"Newly read text from $from:\n$text")) - logger.debug(innerContext.withId(s"New $from output:\n${newRunContext.output}")) - newRunContext + logger.debug(runContext.withId(s"Going to concurrently read from StdOut and StdErr.")) + innerContext.process.readOnFirstInputStream() } - } - } - + matchingWhen map { case (when, newRunContext) => logger.info(newRunContext.withId(s"Matched with:\n$when")) when.run(newRunContext) } - } - - def map[T](f: R => T): ExpectBlock[T] = { - ExpectBlock(whens.map(_.map(f)):_*) - } - def flatMap[T](f: R => Expect[T]): ExpectBlock[T] = { - ExpectBlock(whens.map(_.flatMap(f)):_*) - } - def transform[T](flatMapPF: PartialFunction[R, Expect[T]], mapPF: PartialFunction[R, T]): ExpectBlock[T] = { - ExpectBlock(whens.map(_.transform(flatMapPF, mapPF)):_*) - } + + def map[T](f: R => T): ExpectBlock[T] = ExpectBlock(whens.map(_.map(f))*) + def flatMap[T](f: R => Expect[T]): ExpectBlock[T] = ExpectBlock(whens.map(_.flatMap(f))*) + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): ExpectBlock[T] = ExpectBlock(whens.map(_.transform(flatMapPF, mapPF))*) override def toString: String = s"""expect { |${whens.mkString("\n").indent()} |}""".stripMargin - + /** - * @define subtypes whens - * Returns whether the other $type has the same number of $subtypes as this $type and - * that each pair of $subtypes is structurally equal. + * Returns whether the `other` ExpectBlock has structurally the same whens as this ExpectBlock. * - * @param other the other $type to campare this $type to. + * @param other the other ExpectBlock to campare this ExpectBlock to. */ - def structurallyEquals[RR >: R](other: ExpectBlock[RR]): Boolean = { - whens.size == other.whens.size && /* Fail fast */ - whens.zip(other.whens).forall{ case (a, b) => a.structurallyEquals(b) } - } -} \ No newline at end of file + def structurallyEquals[RR >: R](other: ExpectBlock[RR]): Boolean = + whens.size == other.whens.size && whens.zip(other.whens).forall(_.structurallyEquals(_)) \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/RichProcess.scala b/src/main/scala/work/martins/simon/expect/core/RichProcess.scala index c4faa28..3e74528 100644 --- a/src/main/scala/work/martins/simon/expect/core/RichProcess.scala +++ b/src/main/scala/work/martins/simon/expect/core/RichProcess.scala @@ -3,19 +3,18 @@ package work.martins.simon.expect.core import java.io.{EOFException, IOException} import java.nio.ByteBuffer import java.util.concurrent.{BlockingDeque, LinkedBlockingDeque, TimeUnit} - -import com.typesafe.scalalogging.LazyLogging -import com.zaxxer.nuprocess.{NuAbstractProcessHandler, NuProcess, NuProcessBuilder} -import work.martins.simon.expect.{FromInputStream, Settings, StdErr, StdOut} - import scala.concurrent.duration.Deadline import scala.concurrent.{TimeoutException, blocking} -import scala.collection.JavaConverters._ +import scala.collection.JavaConverters.* +import com.typesafe.scalalogging.LazyLogging +import com.zaxxer.nuprocess.{NuAbstractProcessHandler, NuProcess, NuProcessBuilder} +import work.martins.simon.expect.{FromInputStream, Settings} +import work.martins.simon.expect.FromInputStream.* -trait RichProcess { +trait RichProcess: val command: Seq[String] val settings: Settings - + /** * Tries to read from the selected InputStream of the process. * If no bytes are read within `deadline` a `TimeoutException` should be thrown. @@ -30,7 +29,7 @@ trait RichProcess { * * @return a String created from the read bytes encoded with `charset`. */ - def read(from: FromInputStream = StdOut)(implicit deadline: Deadline): String + def read(from: FromInputStream = StdOut)(using deadline: Deadline): String /** * Tries to read from `StdOut` or `StdErr` of the process, whichever has output to offer first. * If no bytes are read within `timeout` a `TimeoutException` should be thrown. @@ -44,99 +43,87 @@ trait RichProcess { * * @return from which `InputStream` the output read from, and a String created from the read bytes encoded with `charset`. */ - def readOnFirstInputStream()(implicit deadline: Deadline): (FromInputStream, String) - + def readOnFirstInputStream()(using deadline: Deadline): (FromInputStream, String) + /** * Writes to the `StdIn` of the process the bytes obtained from decoding `text` using `settings.charset`. * @param text the text to write to the `OutputStream`. */ def write(text: String): Unit - + /** Destroys the process if it's still alive. */ def destroy(): Unit -} -case class NuProcessRichProcess(command: Seq[String], settings: Settings) extends RichProcess with LazyLogging { +case class NuProcessRichProcess(command: Seq[String], settings: Settings) extends RichProcess with LazyLogging: protected val stdInQueue = new LinkedBlockingDeque[String]() protected val stdOutQueue = new LinkedBlockingDeque[Either[EOFException, String]]() protected val stdErrQueue = new LinkedBlockingDeque[Either[EOFException, String]]() protected val readAvailableOnQueue = new LinkedBlockingDeque[FromInputStream]() - + protected val handler = new ProcessHandler() - val process: NuProcess = new NuProcessBuilder(handler, command:_*).start() - + val process: NuProcess = new NuProcessBuilder(handler, command*).start() + // See https://github.com/brettwooldridge/NuProcess/issues/67 as to why this code exists - if (!handler.startedNormally) throw new IOException() - - class ProcessHandler extends NuAbstractProcessHandler with LazyLogging { - var nuProcess: NuProcess = _ + if !handler.startedNormally then throw new IOException() + + class ProcessHandler extends NuAbstractProcessHandler: + var nuProcess: NuProcess = scala.compiletime.uninitialized var startedNormally = false - - override def onStart(nuProcess: NuProcess): Unit = { + + override def onStart(nuProcess: NuProcess): Unit = this.nuProcess = nuProcess startedNormally = true - } - + private var unwrittenBytes = Array.emptyByteArray - override def onStdinReady(buffer: ByteBuffer): Boolean = { - if (unwrittenBytes.length > 0) { + override def onStdinReady(buffer: ByteBuffer): Boolean = + if unwrittenBytes.length > 0 then // We have unwritten bytes which take precedence over "normal" ones write(buffer, unwrittenBytes) - } else { + else // We know the remove will not throw an exception because onStdinReady will only be called when // process.wantWrite() is invoked. And wantWrite is only invoked inside the method print which: // · First puts a string to the stdInQueue // · Then invokes wantWrite. write(buffer, stdInQueue.remove().getBytes(settings.charset)) - } - } - private def write(buffer: ByteBuffer, bytes: Array[Byte]): Boolean = { - if (bytes.length <= buffer.capacity()) { + private def write(buffer: ByteBuffer, bytes: Array[Byte]): Boolean = + if bytes.length <= buffer.capacity() then buffer.put(bytes) buffer.flip() unwrittenBytes = Array.emptyByteArray false - } else { + else val (bytesWritableRightNow, bytesToBeWritten) = bytes.splitAt(buffer.capacity()) buffer.put(bytesWritableRightNow) buffer.flip() unwrittenBytes = bytesToBeWritten true - } - } - - override def onStdout(buffer: ByteBuffer, closed: Boolean): Unit = { + + override def onStdout(buffer: ByteBuffer, closed: Boolean): Unit = read(buffer, closed, StdOut) - } - override def onStderr(buffer: ByteBuffer, closed: Boolean): Unit = { + override def onStderr(buffer: ByteBuffer, closed: Boolean): Unit = read(buffer, closed, StdErr) - } - private def read(buffer: ByteBuffer, closed: Boolean, readFrom: FromInputStream): Unit = { + private def read(buffer: ByteBuffer, closed: Boolean, readFrom: FromInputStream): Unit = val toQueue = queueOf(readFrom) - + readAvailableOnQueue.putLast(readFrom) logger.trace(s"Read available on $readFrom. DequeReadAvailableOn: ${readAvailableOnQueue.asScala.mkString(", ")}") - - if (closed) { + + if closed then toQueue.put(Left(new EOFException())) - } else if (buffer.hasRemaining) { + else if buffer.hasRemaining then val bytes = Array.ofDim[Byte](buffer.remaining()) buffer.get(bytes) toQueue.put(Right(new String(bytes, settings.charset))) - } - } - } - + /** * The corresponding queue for `from`. * @param from which InputStream to get the queue of. */ - def queueOf(from: FromInputStream = StdOut): BlockingDeque[Either[EOFException, String]] = from match { + def queueOf(from: FromInputStream = StdOut): BlockingDeque[Either[EOFException, String]] = from match case StdErr => stdErrQueue case _ => stdOutQueue - } - - def read(from: FromInputStream = StdOut)(implicit deadline: Deadline): String = { + + def read(from: FromInputStream = StdOut)(using deadline: Deadline): String = Option { blocking { queueOf(from).pollFirst(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) @@ -147,11 +134,10 @@ case class NuProcessRichProcess(command: Seq[String], settings: Settings) extend // later `readOnFirstInputStream` is performed it will get the correct value. readAvailableOnQueue.removeFirstOccurrence(from) logger.trace(s"Read: took ReadAvailableOn $from. ReadAvailableOnQueue: ${readAvailableOnQueue.asScala.mkString(", ")}") - + result.fold(throw _, identity) }.getOrElse(throw new TimeoutException()) - } - def readOnFirstInputStream()(implicit deadline: Deadline): (FromInputStream, String) = { + def readOnFirstInputStream()(using deadline: Deadline): (FromInputStream, String) = Option{ blocking { readAvailableOnQueue.pollFirst(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) @@ -164,28 +150,22 @@ case class NuProcessRichProcess(command: Seq[String], settings: Settings) extend } }.map(_.fold(throw _, (from, _))) }.getOrElse(throw new TimeoutException()) - } - - def write(text: String): Unit = if (process.isRunning) { + + def write(text: String): Unit = if process.isRunning then stdInQueue.put(text) process.wantWrite() - } - - def destroy(): Unit = if (process.isRunning) { - try { + + def destroy(): Unit = if process.isRunning then + try // First allow the process to terminate gracefully process.destroy(false) // Check whether it terminated or not val returnCode = blocking(process.waitFor(settings.timeout.toMillis, TimeUnit.MILLISECONDS)) - if (returnCode == Integer.MIN_VALUE) { + if returnCode == Integer.MIN_VALUE then // The waitFor timed out. Ensure the process terminates by forcing it. process.destroy(true) - } - } catch { + catch case e: RuntimeException if e.getMessage.contains("Sending signal failed") => // There is a bug in NuProcess where sometimes `process.destroy(false)` throws a RuntimeException // with the message "Sending signal failed, return code: -1, last error: 3". // In this case we assume the process already finished - } - } -} diff --git a/src/main/scala/work/martins/simon/expect/core/RunContext.scala b/src/main/scala/work/martins/simon/expect/core/RunContext.scala index c166c32..d556b7a 100644 --- a/src/main/scala/work/martins/simon/expect/core/RunContext.scala +++ b/src/main/scala/work/martins/simon/expect/core/RunContext.scala @@ -1,24 +1,22 @@ package work.martins.simon.expect.core -import com.typesafe.scalalogging.LazyLogging -import work.martins.simon.expect.core.RunContext.ExecutionAction -import work.martins.simon.expect.{FromInputStream, StdErr, StdOut} +import work.martins.simon.expect.FromInputStream +import work.martins.simon.expect.FromInputStream.* import scala.util.Random -object RunContext { - sealed trait ExecutionAction - case object Terminate extends ExecutionAction - case object Continue extends ExecutionAction - case class ChangeToNewExpect[R](expect: Expect[R]) extends ExecutionAction -} +enum ExecutionAction derives CanEqual: + case Terminate + case Continue + case ChangeToNewExpect[R](expect: Expect[R]) + final case class RunContext[+R](process: RichProcess, value: R, executionAction: ExecutionAction, readFrom: FromInputStream = StdOut, stdOutOutput: String = "", stdErrOutput: String = "", - id: String = Random.nextInt.toString) extends LazyLogging { - - //Shortcut, because it makes sense + id: String = Random.nextInt.toString): + + // Shortcut, because it makes sense val settings = process.settings - + /** * When running multiple Expects simultaneously its hard to differentiate them. The `id` helps to do so. * Currently the `id` is just a random number. It's not the Expect hashCode because we could be running the same @@ -27,17 +25,14 @@ final case class RunContext[+R](process: RichProcess, value: R, executionAction: * @return */ def withId(message: String): String = s"[ID:$id] $message" - + def output: String = outputOf(readFrom) - def outputOf(from: FromInputStream): String = from match { + def outputOf(from: FromInputStream): String = from match case StdErr => stdErrOutput case StdOut => stdOutOutput - } - - def withOutput(f: String => String): RunContext[R] = readFrom match { - case StdErr => this.copy(stdErrOutput = f(stdErrOutput)) - case StdOut => this.copy(stdOutOutput = f(stdOutOutput)) - } - - def readingFrom(readFrom: FromInputStream): RunContext[R] = this.copy(readFrom = readFrom) -} + + def withOutput(f: String => String): RunContext[R] = readFrom match + case StdErr => copy(stdErrOutput = f(stdErrOutput)) + case StdOut => copy(stdOutOutput = f(stdOutOutput)) + + def readingFrom(readFrom: FromInputStream): RunContext[R] = copy(readFrom = readFrom) \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/Whens.scala b/src/main/scala/work/martins/simon/expect/core/Whens.scala index 4597635..c3d68b1 100644 --- a/src/main/scala/work/martins/simon/expect/core/Whens.scala +++ b/src/main/scala/work/martins/simon/expect/core/Whens.scala @@ -1,262 +1,157 @@ package work.martins.simon.expect.core -import com.typesafe.scalalogging.LazyLogging -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect.core.actions.Action -import work.martins.simon.expect.{EndOfFile, FromInputStream, StdOut, Timeout} - -import scala.language.higherKinds import scala.util.matching.Regex - -object When { - /* - trait WhenBuilder[PT, R, W[+X] <: When[X]] { - def build(s: PT, f: FromInputStream): Seq[Action[R, W]] => W[R] - } - - // SAMs for the win! - implicit def stringWhenBuilder[R]: WhenBuilder[String, R, StringWhen] = StringWhen.apply[R] - implicit def regexWhenBuilder[R]: WhenBuilder[Regex, R, RegexWhen] = RegexWhen.apply[R] - implicit def endOfFileWhenBuilder[R]: WhenBuilder[EndOfFile.type, R, EndOfFileWhen] = (_, from) => EndOfFileWhen[R](from) - implicit def timeoutWhenBuilder[R]: WhenBuilder[Timeout.type, R, TimeoutWhen] = (_, _) => TimeoutWhen.apply[R]() - - def apply[PT, R, W[+X] <: When[X]](pattern: PT, readFrom: FromInputStream = StdOut) - (actions: Action[R, W]*)(implicit whenBuilder: WhenBuilder[PT, R, W]): W[R] = { - whenBuilder.build(pattern, readFrom)(actions) - } - */ +import scala.util.matching.Regex.Match +import work.martins.simon.expect./=> +import work.martins.simon.expect.StringUtils.* +import work.martins.simon.expect.core.actions.Action +import work.martins.simon.expect.{EndOfFile, FromInputStream, Timeout} +import work.martins.simon.expect.FromInputStream.StdOut +import work.martins.simon.expect.core.ExecutionAction.* + +object When: + def apply[R](pattern: String)(actions: Action[R, StringWhen]*): StringWhen[R] = + StringWhen(pattern, actions = actions*) + def apply[R](pattern: String, readFrom: FromInputStream)(actions: Action[R, StringWhen]*): StringWhen[R] = + StringWhen(pattern, readFrom, actions*) - def apply[R](pattern: String)(actions: Action[R, StringWhen]*): StringWhen[R] = StringWhen(pattern)(actions:_*) - def apply[R](pattern: String, readFrom: FromInputStream)(actions: Action[R, StringWhen]*): StringWhen[R] = { - StringWhen(pattern, readFrom)(actions:_*) - } + def apply[R](pattern: Regex)(actions: Action[R, RegexWhen]*): RegexWhen[R] = + RegexWhen(pattern, actions = actions*) + def apply[R](pattern: Regex, readFrom: FromInputStream)(actions: Action[R, RegexWhen]*): RegexWhen[R] = + RegexWhen(pattern, readFrom, actions*) - def apply[R](pattern: Regex)(actions: Action[R, RegexWhen]*): RegexWhen[R] = RegexWhen(pattern)(actions:_*) - def apply[R](pattern: Regex, readFrom: FromInputStream)(actions: Action[R, RegexWhen]*): RegexWhen[R] = { - RegexWhen(pattern, readFrom)(actions:_*) - } + // Pattern is not used but we need it to disambiguate between creating a EndOfFileWhen vs a TimeoutWhen. + def apply[R](pattern: EndOfFile.type, readFrom: FromInputStream = StdOut)(actions: Action[R, EndOfFileWhen]*): EndOfFileWhen[R] = + EndOfFileWhen(readFrom, actions*) - // We need to include EndOfFile and Timeout because otherwise this would be ambiguous (although the scala compiler doesn't think so): - // def apply[R](readFrom: FromInputStream = StdOut)(actions: Action[R, EndOfFileWhen]*): EndOfFileWhen[R] = { - // EndOfFileWhen(readFrom)(actions:_*) - // } - // def apply[R]()(actions: Action[R, TimeoutWhen]*): TimeoutWhen[R] = TimeoutWhen()(actions:_*) - - import com.github.ghik.silencer.silent - // the pattern is not used but we need it to disambiguate between creating a EndOfFileWhen vs a TimeoutWhen. One could say its an erased term. - def apply[R](@silent pattern: EndOfFile.type, readFrom: FromInputStream = StdOut)(actions: Action[R, EndOfFileWhen]*): EndOfFileWhen[R] = { - EndOfFileWhen(readFrom)(actions:_*) - } - def apply[R](@silent pattern: Timeout.type)(actions: Action[R, TimeoutWhen]*): TimeoutWhen[R] = TimeoutWhen()(actions:_*) -} + def apply[R](pattern: Timeout.type)(actions: Action[R, TimeoutWhen]*): TimeoutWhen[R] = + TimeoutWhen(actions*) -/** - * @define type `When` - */ -sealed trait When[+R] extends LazyLogging { - /** The concrete $type type constructor to which the actions will be applied. */ +sealed trait When[+R]: + /** The concrete When type constructor to which the actions will be applied. */ type This[+X] <: When[X] - + /** From which InputStream to read text from. */ def readFrom: FromInputStream - /** The actions this $type runs when it matches against the output read from `readFrom` InputStream. */ + /** The actions this when runs when it matches against the output read from `readFrom` InputStream. */ def actions: Seq[Action[R, This]] - + /** - * @param output the String to match against. - * @return whether this $type matches against `output`. - */ - def matches(output: String): Boolean - + * @param output the String to match against. + * @return whether this When matches against `output`. + */ + def matches(output: String): Boolean = false + /** - * @param output the output to trim. - * @return the text in `output` that remains after removing all the text up to and including the first occurrence - * of the text matched by this $type. - */ + * @param output the output to trim. + * @return the text in `output` that remains after removing all the text up to and including the first occurrence + * of the text matched by this When. + */ def trimToMatchedText(output: String): String = output - - /** Runs all the actions of this $type. */ - private[core] def run[RR >: R](runContext: RunContext[RR]): RunContext[RR] = { - import work.martins.simon.expect.core.RunContext.{ChangeToNewExpect, Continue, Terminate} - + + /** Runs all the actions of this When. */ + private[core] def run[RR >: R](runContext: RunContext[RR]): RunContext[RR] = + import work.martins.simon.expect.core.ExecutionAction.* import scala.annotation.tailrec @tailrec - def runInner(actions: Seq[Action[RR, This]], innerRunContext: RunContext[RR]): RunContext[RR] = { - actions.headOption match { + def runInner(actions: Seq[Action[RR, This]], innerRunContext: RunContext[RR]): RunContext[RR] = + actions.headOption match case Some(action) => val newRunContext = action.run(this.asInstanceOf[This[RR]], innerRunContext) - newRunContext.executionAction match { + newRunContext.executionAction match case Continue => runInner(actions.tail, newRunContext) case Terminate | ChangeToNewExpect(_) => // We just return the new run context to do a preemptive exit, ie, ensure // anything after this action does not get executed newRunContext - } case None => // No more actions. We just return the current RunContext innerRunContext - } - } - + runInner(actions, runContext).withOutput(trimToMatchedText) - } - + def map[T](f: R => T): This[T] = withActions(actions.map(_.map(f))) def flatMap[T](f: R => Expect[T]): This[T] = withActions(actions.map(_.flatMap(f))) - def transform[T](flatMapPF: PartialFunction[R, Expect[T]], mapPF: PartialFunction[R, T]): This[T] = { - withActions(actions.map(_.transform(flatMapPF, mapPF))) - } - - /** Create a new $type with the specified actions. */ + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): This[T] = withActions(actions.map(_.transform(flatMapPF, mapPF))) + + /** Create a new When with the specified actions. */ def withActions[T](actions: Seq[Action[T, This]]): This[T] - + /** - * @define subtypes actions - * Returns whether the other $type has the same number of $subtypes as this $type and - * that each pair of $subtypes is structurally equal. - * - * @param other the other $type to compare this $type to. + * Returns whether the `other` When has structurally the same actions as this When. + * @param other the other When to compare this When to. */ - def structurallyEquals[RR >: R](other: When[RR]): Boolean = { - actions.size == other.actions.size && actions.zip(other.actions).forall { case (a, b) => a.structurallyEquals(b) } && - readFrom == other.readFrom - } - - protected def toString(pattern: String): String = { + def structurallyEquals[RR >: R](other: When[RR]): Boolean = + readFrom == other.readFrom && actions.size == other.actions.size && + actions.zip(other.actions).forall(_.structurallyEquals(_)) + + protected def toString(pattern: String): String = s"""when($pattern, readFrom = $readFrom) { |${actions.mkString("\n").indent()} |}""".stripMargin - } -} -final case class StringWhen[+R](pattern: String, readFrom: FromInputStream = StdOut)(val actions: Action[R, StringWhen]*) extends When[R] { +final case class StringWhen[+R](pattern: String, readFrom: FromInputStream = StdOut, actions: Action[R, StringWhen]*) extends When[R] derives CanEqual: type This[+X] = StringWhen[X] - + override def matches(output: String): Boolean = output.contains(pattern) - override def trimToMatchedText(output: String): String = { - output.substring(output.indexOf(pattern) + pattern.length) - } - - def withActions[T](actions: Seq[Action[T, This]]): StringWhen[T] = StringWhen(pattern, readFrom)(actions:_*) - + + override def trimToMatchedText(output: String): String = output.substring(output.indexOf(pattern) + pattern.length) + + def withActions[T](actions: Seq[Action[T, This]]): StringWhen[T] = StringWhen(pattern, readFrom, actions*) + override def toString: String = toString(escape(pattern)) - override def equals(other: Any): Boolean = other match { - case that: StringWhen[R] => pattern == that.pattern && readFrom == that.readFrom && actions == that.actions + override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match + case that: StringWhen[RR] => pattern == that.pattern && super.structurallyEquals(other) case _ => false - } - override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match { - case that: StringWhen[R] => pattern == that.pattern && super.structurallyEquals(other) - case _ => false - } - override def hashCode(): Int = { - Seq(pattern, readFrom, actions) - .map(_.hashCode()) - .foldLeft(0)((a, b) => 31 * a + b) - } -} -final case class RegexWhen[+R](pattern: Regex, readFrom: FromInputStream = StdOut)(val actions: Action[R, RegexWhen]*) extends When[R] { + +final case class RegexWhen[+R](pattern: Regex, readFrom: FromInputStream = StdOut, actions: Action[R, RegexWhen]*) extends When[R] derives CanEqual: type This[+X] = RegexWhen[X] - import scala.util.matching.Regex.Match - override def matches(output: String): Boolean = pattern.findFirstIn(output).isDefined + override def trimToMatchedText(output: String): String = output.substring(regexMatch(output).end(0)) - - protected[core] def regexMatch(output: String): Match = { + + protected[core] def regexMatch(output: String): Match = //We have the guarantee that .get will be successful because this method //is only invoked if `matches` returned true. pattern.findFirstMatchIn(output).get - } - - def withActions[T](actions: Seq[Action[T, This]]): RegexWhen[T] = RegexWhen(pattern, readFrom)(actions:_*) - + + def withActions[T](actions: Seq[Action[T, This]]): RegexWhen[T] = RegexWhen(pattern, readFrom, actions *) + override def toString: String = toString(escape(pattern.regex) + ".r") - override def equals(other: Any): Boolean = other match { - case that: RegexWhen[R] => pattern.regex == that.pattern.regex && readFrom == that.readFrom && actions == that.actions + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + // equals on the Regex class is not defined. + case that: RegexWhen[?] => pattern.regex == that.pattern.regex && readFrom == that.readFrom && actions == that.actions case _ => false - } - override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match { - case that: RegexWhen[R] => pattern.regex == that.pattern.regex && super.structurallyEquals(other) + override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match + case that: RegexWhen[RR] => pattern.regex == that.pattern.regex && super.structurallyEquals(other) case _ => false - } - override def hashCode(): Int = { - Seq(pattern, readFrom, actions) - .map(_.hashCode()) - .foldLeft(0)((a, b) => 31 * a + b) - } -} - -// EndOfFileWhen and TimeoutWhen are special whens because they will only be used inside the same ExpectBlock: -// In this case: -// ExpectBlock ( -// StringWhen(...)( -// ... -// ), -// TimeoutWhen(...)( -// ... -// ) -// ) -// If a timeout is thrown while trying to read text for the StringWhen then the actions of the TimeoutWhen will be -// executed. However in this case (considering the expect block with the StringWhen also times out): -// ExpectBlock ( -// StringWhen(...)( -// ... -// ) -// ), ExpectBlock( -// TimeoutWhen(...)( -// ... -// ) -// ) -// The TimeoutWhen is useless since the ExpectBlock with the StringWhen will timeout and without having a TimeoutWhen -// declared in its whens will throw a TimeoutExpect causing the expect to terminate and therefor the next expect block -// (the one with the timeout when) will never be executed. -// TODO: explain this caveat in their scaladoc -final case class EndOfFileWhen[+R](readFrom: FromInputStream = StdOut)(val actions: Action[R, EndOfFileWhen]*) extends When[R] { +final case class EndOfFileWhen[+R](readFrom: FromInputStream = StdOut, actions: Action[R, EndOfFileWhen]*) extends When[R] derives CanEqual: type This[+X] = EndOfFileWhen[X] - - override def matches(output: String) = false - - def withActions[T](actions: Seq[Action[T, This]]): EndOfFileWhen[T] = EndOfFileWhen(readFrom)(actions:_*) + + def withActions[T](actions: Seq[Action[T, This]]): EndOfFileWhen[T] = EndOfFileWhen(readFrom, actions *) override def toString: String = toString("EndOfFile") - override def equals(other: Any): Boolean = other match { - case that: EndOfFileWhen[R] => readFrom == that.readFrom && actions == that.actions + override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match + case _: EndOfFileWhen[RR] => super.structurallyEquals(other) case _ => false - } - override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match { - case _: EndOfFileWhen[R] => super.structurallyEquals(other) - case _ => false - } - override def hashCode(): Int = { - Seq(readFrom, actions) - .map(_.hashCode()) - .foldLeft(0)((a, b) => 31 * a + b) - } -} -final case class TimeoutWhen[+R]()(val actions: Action[R, TimeoutWhen]*) extends When[R] { - type This[+X] = TimeoutWhen[X] - override def matches(output: String) = false +final case class TimeoutWhen[+R](actions: Action[R, TimeoutWhen]*) extends When[R] derives CanEqual: + type This[+X] = TimeoutWhen[X] /** The readFrom of a TimeoutWhen is not used but to keep the implementation simple we set its value to StdOut. */ val readFrom: FromInputStream = StdOut - def withActions[T](actions: Seq[Action[T, This]]): TimeoutWhen[T] = TimeoutWhen()(actions:_*) + def withActions[T](actions: Seq[Action[T, This]]): TimeoutWhen[T] = TimeoutWhen(actions *) - override def toString: String = s"""when(Timeout) { - |${actions.mkString("\n").indent()} - |}""".stripMargin - override def equals(other: Any): Boolean = other match { - case that: TimeoutWhen[R] => actions == that.actions - case _ => false - } - override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match { - case _: TimeoutWhen[R] => super.structurallyEquals(other) - case _ => false - } - override def hashCode(): Int = actions.hashCode() -} + override def toString: String = + s"""when(Timeout) { + |${actions.mkString("\n").indent()} + |}""".stripMargin + override def structurallyEquals[RR >: R](other: When[RR]): Boolean = other match + case _: TimeoutWhen[RR] => super.structurallyEquals(other) + case _ => false \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/actions/Action.scala b/src/main/scala/work/martins/simon/expect/core/actions/Action.scala index f51d6a2..08260aa 100644 --- a/src/main/scala/work/martins/simon/expect/core/actions/Action.scala +++ b/src/main/scala/work/martins/simon/expect/core/actions/Action.scala @@ -1,46 +1,50 @@ package work.martins.simon.expect.core.actions -import work.martins.simon.expect.core.{RunContext, Expect, When} - -import scala.language.higherKinds +import work.martins.simon.expect./=> +import work.martins.simon.expect.core.{Expect, RunContext, When} /** - * @define type Action - * @define regexWhen This $type can only be added to a RegexWhen. - * @define returningAction When this $type is executed the result of evaluating `result` is returned by - * the current run of Expect. + * @define regexWhen This Action can only be added to a RegexWhen. + * @define returningAction When this Action is executed the result of evaluating `result` is returned by the current run of Expect. * @define moreThanOne If more than one returning action is added to a When only the last `result` will be returned. * Note however that every returning action will still be executed. * @tparam W the concrete When type constructor to which this action can be applied. + * For example `SendWithRegex` can only be used in `RegexWhen`s. */ -trait Action[+R, -W[+X] <: When[X]] { +trait Action[+R, -W[+X] <: When[X]] derives CanEqual: def run[RR >: R](when: W[RR], runContext: RunContext[RR]): RunContext[RR] - + def map[T](f: R => T): Action[T, W] def flatMap[T](f: R => Expect[T]): Action[T, W] - type /=>[-A, +B] = PartialFunction[A, B] def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): Action[T, W] - - protected def pfNotDefined[RR >: R, T](r: RR): T = { + + protected def pfNotDefined[RR >: R, T](r: RR): T = throw new NoSuchElementException(s"Expect.transform neither flatMapPF nor mapPF are defined at $r (from ${this.getClass.getSimpleName})") - } - + /** - * Returns whether the other $type is structurally equal to this $type. + * Compares two actions structurally. * * @example {{{ + * Exit() structurallyEquals Send("done") //returns false * Exit() structurallyEquals Exit() //returns true - * Exit() structurallyEquals Send() //returns false * Send("AA") structurallyEquals Send("BB") //returns true + * Sendln(m => s"${m.group(1)} + 3") structurallyEquals Sendln(_ => "random string") //returns true + * Sendln(m => s"${m.group(1)} + 3") structurallyEquals Sendln("random string") //returns false * Returning(5) structurallyEquals Returning { complexFunctionReturningAnInt } //returns true * Returning("AA") structurallyEquals Returning(5) //won't compile * }}} * - * We cannot test that an $type is equal to another $type because some actions return a value by invoking a function. - * And equality on a function is not defined. So in other to extract some useful information about the "equality" of - * two actions we created this function. + * `Action.equals` is misleading for actions that return a value by invoking a function (eg: SendWithRegex, Returning), since equality + * on functions is undecidable. This function allows extracting some useful information about the "equality" of two actions. * - * @param other the other $type to campare this $type to. + * @param other the other action to campare this action to. */ - def structurallyEquals[RR >: R, WW[+X] <: W[X]](other: Action[RR, WW]): Boolean -} + def structurallyEquals[RR >: R](other: Action[RR, ?]): Boolean + +/** An action which does not produce any values but its rather executed for its side-effects. */ +trait NonProducingAction[-W[+X] <: When[X]] extends Action[Nothing, W] derives CanEqual: + def run[RR >: Nothing](when: W[RR], runContext: RunContext[RR]): RunContext[RR] + + def map[T](f: Nothing => T): Action[T, W] = this + def flatMap[T](f: Nothing => Expect[T]): Action[T, W] = this + def transform[T](flatMapPF: Nothing /=> Expect[T], mapPF: Nothing /=> T): Action[T, W] = this \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/actions/Exit.scala b/src/main/scala/work/martins/simon/expect/core/actions/Exit.scala index bbde46c..8ff210d 100644 --- a/src/main/scala/work/martins/simon/expect/core/actions/Exit.scala +++ b/src/main/scala/work/martins/simon/expect/core/actions/Exit.scala @@ -1,27 +1,17 @@ package work.martins.simon.expect.core.actions -import scala.language.higherKinds - -import work.martins.simon.expect.core.RunContext.Terminate -import work.martins.simon.expect.core._ +import work.martins.simon.expect./=> +import work.martins.simon.expect.core.* +import work.martins.simon.expect.core.ExecutionAction.Terminate /** * When this action is executed the current run of Expect is terminated causing it to return the - * last value, if there is a ReturningAction, or the default value otherwise. + * last value if there is a ReturningAction, or the default value otherwise. * * Any action or expect block added after this will not be executed. */ -final case class Exit[+R]() extends Action[R, When] { - def run[RR >: R](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = { +final case class Exit() extends NonProducingAction[When]: + def run[RR >: Nothing](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = runContext.copy(executionAction = Terminate) - } - - //These methods just perform a cast because the type argument R is just used here, - //so there isn't the need to allocate need objects. - - def map[T](f: R => T): Action[T, When] = this.asInstanceOf[Exit[T]] - def flatMap[T](f: R => Expect[T]): Action[T, When] = this.asInstanceOf[Exit[T]] - def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): Action[T, When] = this.asInstanceOf[Exit[T]] - - def structurallyEquals[RR >: R, W[+X] <: When[X]](other: Action[RR, W]): Boolean = other.isInstanceOf[Exit[RR]] -} + + def structurallyEquals[RR >: Nothing](other: Action[RR, ?]): Boolean = other.isInstanceOf[Exit] \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/actions/Returning.scala b/src/main/scala/work/martins/simon/expect/core/actions/Returning.scala index 705c1d8..a43f5b5 100644 --- a/src/main/scala/work/martins/simon/expect/core/actions/Returning.scala +++ b/src/main/scala/work/martins/simon/expect/core/actions/Returning.scala @@ -1,69 +1,90 @@ package work.martins.simon.expect.core.actions -import work.martins.simon.expect.core.RunContext.ChangeToNewExpect -import work.martins.simon.expect.core._ - -import scala.language.higherKinds import scala.util.matching.Regex.Match +import work.martins.simon.expect./=> +import work.martins.simon.expect.core.* +import work.martins.simon.expect.core.ExecutionAction.ChangeToNewExpect -sealed trait AbstractReturning[+WR] extends Action[WR, When] { - override def map[T](f: WR => T): AbstractReturning[T] - override def flatMap[T](f: WR => Expect[T]): AbstractReturning[T] - override def transform[T](flatMapPF: WR /=> Expect[T], mapPF: WR /=> T): AbstractReturning[T] -} - -// Returning cannot be declared: -// case class Returning(result: => R) extends AbstractReturning[R] -// because `val' parameters may not be call-by-name. To solve this problem we declared Returning with result -// type as Unit => R. This implies we do not need to implement the sensitive flag like in Send. - -object Returning { - // Scala auto generates applys of private constructors of case classes. We do not want that. - // So we define it just to make it private. We use it just for the coverage. - private def apply[R](result: Unit => R): Returning[R] = new Returning(result) +object Returning: def apply[R](result: => R): Returning[R] = Returning(_ => result) def apply[R](result: Match => R): ReturningWithRegex[R] = ReturningWithRegex(result) -} +object ReturningExpect: + def apply[R](result: => Expect[R]): ReturningExpect[R] = ReturningExpect(_ => result) + def apply[R](result: Match => Expect[R]): ReturningExpectWithRegex[R] = ReturningExpectWithRegex(result) + +// `result` is not by-name because "val parameters may not be call-by-name", to solve this problem: +// 1. `apply` in the companion object has `result` by-name. +// 2. The constructor is private and `result` has type `Unit => R` (notice its not `() => R`), this solves multiple problems: +// · Having the constructor private disambiguates between the overloaded alternatives `Match => R` and `=> R` in the companion object. +// · `() => R` and `=> R` have the same type after erasure (Function0), whereas `Unit => R` has type `Function1[Unit, R]` +// this way the compiler does not complain about a double definition. +// · The sensitive flag comes for free. /** * $returningAction * $moreThanOne **/ -final case class Returning[+R] private (result: Unit => R) extends AbstractReturning[R] { - // The constructor is Unit => R so it does not collide with the apply. - - def run[RR >: R](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = { +final case class Returning[+R] private (result: Unit => R) extends Action[R, When] derives CanEqual: + def run[RR >: R](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = runContext.copy(value = result(())) - } - - def map[T](f: R => T): AbstractReturning[T] = { - this.copy(result andThen f) - } - def flatMap[T](f: R => Expect[T]): AbstractReturning[T] = { - ReturningExpect(f(result(()))) - } - def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): AbstractReturning[T] = { - val computeAction: R => AbstractReturning[T] = { - // We cannot use the map/flatMap because if we did the returning result would be ran twice in the ActionReturningAction: - // · once inside the execute which invokes parent.result + + def map[T](f: R => T): Returning[T] = copy(result andThen f) + def flatMap[T](f: R => Expect[T]): ReturningExpect[T] = ReturningExpect(f(result(()))) + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): ActionReturningAction[T] = + ActionReturningAction(result andThen { + // We cannot use the map/flatMap because if we did `result` would be ran twice: + // · once inside ActionReturningAction.run which invokes `result(())` // · and another when the action returned by the ActionReturningAction is ran case r if flatMapPF.isDefinedAt(r) => ReturningExpect(flatMapPF(r)) - case r if mapPF.isDefinedAt(r) => this.copy(_ => mapPF(r)) - case r => pfNotDefined[R, AbstractReturning[T]](r) - } - ActionReturningAction(this, computeAction) - } - - def structurallyEquals[RR >: R, W[+X] <: When[X]](other: Action[RR, W]): Boolean = other.isInstanceOf[Returning[RR]] -} - + case r if mapPF.isDefinedAt(r) => copy(_ => mapPF(r)) + case r => pfNotDefined(r) + }) + + def structurallyEquals[RR >: R](other: Action[RR, ?]): Boolean = other.isInstanceOf[Returning[RR]] +private final case class ActionReturningAction[+R](result: Unit => Action[R, When]) extends Action[R, When] derives CanEqual: + def run[RR >: R](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = + result(()).run(when, runContext) + + def map[T](f: R => T): ActionReturningAction[T] = copy(result.andThen(_.map(f))) + def flatMap[T](f: R => Expect[T]): ActionReturningAction[T] = copy(result.andThen(_.flatMap(f))) + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): ActionReturningAction[T] = copy(result.andThen(_.transform(flatMapPF, mapPF))) + + def structurallyEquals[RR >: R](other: Action[RR, ?]): Boolean = other.isInstanceOf[ActionReturningAction[RR]] -object ReturningExpect { - // See the explanation in object Returning as to why this apply is here. - private def apply[R](result: Unit => Expect[R]): ReturningExpect[R] = new ReturningExpect(result) - def apply[R](result: => Expect[R]): ReturningExpect[R] = ReturningExpect(_ => result) - def apply[R](result: Match => Expect[R]): ReturningExpectWithRegex[R] = ReturningExpectWithRegex(result) -} +/** + * $returningAction + * This allows to return data based on the regex Match. + * $regexWhen + * $moreThanOne + */ +final case class ReturningWithRegex[+R](result: Match => R) extends Action[R, RegexWhen] derives CanEqual: + def run[RR >: R](when: RegexWhen[RR], runContext: RunContext[RR]): RunContext[RR] = + val newValue = result(when.regexMatch(runContext.output)) + runContext.copy(value = newValue) + + def map[T](f: R => T): ReturningWithRegex[T] = copy(result andThen f) + def flatMap[T](f: R => Expect[T]): ReturningExpectWithRegex[T] = ReturningExpectWithRegex(result andThen f) + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): ActionReturningActionWithRegex[T] = + ActionReturningActionWithRegex(result andThen { + //We cannot invoke map/flatMap because if we did `result` would be ran twice: + // · once inside ActionReturningActionWithRegex.run which invokes `result(())` + // · and another when the action returned by the ActionReturningActionWithRegex is ran + case r if flatMapPF.isDefinedAt(r) => ReturningExpectWithRegex(_ => flatMapPF(r)) + case r if mapPF.isDefinedAt(r) => copy(_ => mapPF(r)) + case r => pfNotDefined(r) + }) + + def structurallyEquals[RR >: R](other: Action[RR, ?]): Boolean = other.isInstanceOf[ReturningWithRegex[RR]] +private final case class ActionReturningActionWithRegex[+R](result: Match => Action[R, RegexWhen]) extends Action[R, RegexWhen] derives CanEqual: + def run[RR >: R](when: RegexWhen[RR], runContext: RunContext[RR]): RunContext[RR] = + val regexMatch = when.regexMatch(runContext.output) + result(regexMatch).run(when, runContext) + + def map[T](f: R => T): ActionReturningActionWithRegex[T] = copy(result.andThen(_.map(f))) + def flatMap[T](f: R => Expect[T]): ActionReturningActionWithRegex[T] = copy(result.andThen(_.flatMap(f))) + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): ActionReturningActionWithRegex[T] = copy(result.andThen(_.transform(flatMapPF, mapPF))) + + def structurallyEquals[RR >: R](other: Action[RR, ?]): Boolean = other.isInstanceOf[ActionReturningActionWithRegex[RR]] /** * When this action is executed: @@ -79,42 +100,40 @@ object ReturningExpect { * * Any action or expect block added after this will not be executed. */ -final case class ReturningExpect[+R] private (result: Unit => Expect[R]) extends AbstractReturning[R] { - // The constructor is Unit => R so it does not collide with the apply. - - def run[RR >: R](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = { +final case class ReturningExpect[+R] private (result: Unit => Expect[R]) extends Action[R, When] derives CanEqual: + def run[RR >: R](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = val newExpect = result(()) runContext.copy(executionAction = ChangeToNewExpect(newExpect)) - } - - def map[T](f: R => T): AbstractReturning[T] = { - this.copy(result.andThen(_.map(f))) - } - def flatMap[T](f: R => Expect[T]): AbstractReturning[T] = { - this.copy(result.andThen(_.flatMap(f))) - } - def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): AbstractReturning[T] = { - this.copy(result.andThen(_.transform(flatMapPF, mapPF))) - } - - def structurallyEquals[RR >: R, W[+X] <: When[X]](other: Action[RR, W]): Boolean = this.isInstanceOf[ReturningExpect[RR]] -} - -final case class ActionReturningAction[R, +T](parent: Returning[R], resultAction: R => AbstractReturning[T]) extends AbstractReturning[T] { - def run[TT >: T](when: When[TT], runContext: RunContext[TT]): RunContext[TT] = { - val parentResult: R = parent.result(()) - resultAction(parentResult).run(when, runContext) - } - - def map[U](f: T => U): AbstractReturning[U] = { - this.copy(parent, resultAction.andThen(_.map(f))) - } - def flatMap[U](f: T => Expect[U]): AbstractReturning[U] = { - this.copy(parent, resultAction.andThen(_.flatMap(f))) - } - def transform[U](flatMapPF: T /=> Expect[U], mapPF: T /=> U): AbstractReturning[U] = { - this.copy(parent, resultAction.andThen(_.transform(flatMapPF, mapPF))) - } + + def map[T](f: R => T): ReturningExpect[T] = copy(result.andThen(_.map(f))) + def flatMap[T](f: R => Expect[T]): ReturningExpect[T] = copy(result.andThen(_.flatMap(f))) + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): ReturningExpect[T] = copy(result.andThen(_.transform(flatMapPF, mapPF))) + + def structurallyEquals[RR >: R](other: Action[RR, ?]): Boolean = other.isInstanceOf[ReturningExpect[RR]] - def structurallyEquals[TT >: T, W[+X] <: When[X]](other: Action[TT, W]): Boolean = other.isInstanceOf[ActionReturningAction[R, TT]] -} \ No newline at end of file +/** + * When this action is executed: + * + * 1. The current run of Expect is terminated (like with an `Exit`) but its return value is discarded. + * 2. `result` is evaluated to obtain the expect. + * 3. The obtained expect is run. + * 4. The result obtained in the previous step becomes the result of the current expect (the terminated one). + * + * This works out as a special combination of `Exit` and `Returning`. Where the exit deallocates the + * resources allocated by the current expect. And the result of the `Returning` is obtained from the result of + * executing the received expect. + * + * This allows to construct the Expect based on the regex Match. + * $regexWhen + * Any action or expect block added after this will not be executed. + */ +final case class ReturningExpectWithRegex[+R](result: Match => Expect[R]) extends Action[R, RegexWhen] derives CanEqual: + def run[RR >: R](when: RegexWhen[RR], runContext: RunContext[RR]): RunContext[RR] = + val newExpect = result(when.regexMatch(runContext.output)) + runContext.copy(executionAction = ChangeToNewExpect(newExpect)) + + def map[T](f: R => T): ReturningExpectWithRegex[T] = copy(result.andThen(_.map(f))) + def flatMap[T](f: R => Expect[T]): ReturningExpectWithRegex[T] = copy(result.andThen(_.flatMap(f))) + def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): ReturningExpectWithRegex[T] = copy(result.andThen(_.transform(flatMapPF, mapPF))) + + def structurallyEquals[RR >: R](other: Action[RR, ?]): Boolean = other.isInstanceOf[ReturningExpectWithRegex[RR]] \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/actions/ReturningWithRegex.scala b/src/main/scala/work/martins/simon/expect/core/actions/ReturningWithRegex.scala deleted file mode 100644 index 9ba3739..0000000 --- a/src/main/scala/work/martins/simon/expect/core/actions/ReturningWithRegex.scala +++ /dev/null @@ -1,113 +0,0 @@ -package work.martins.simon.expect.core.actions - -import scala.language.higherKinds -import scala.util.matching.Regex.Match - -import work.martins.simon.expect.core.RunContext.ChangeToNewExpect -import work.martins.simon.expect.core.{RegexWhen, _} - -sealed trait AbstractReturningWithRegex[+WR] extends Action[WR, RegexWhen] { - override def map[T](f: WR => T): AbstractReturningWithRegex[T] - override def flatMap[T](f: WR => Expect[T]): AbstractReturningWithRegex[T] - override def transform[T](flatMapPF: WR /=> Expect[T], mapPF: WR /=> T): AbstractReturningWithRegex[T] -} - -/** - * $returningAction - * This allows to return data based on the regex Match. - * $regexWhen - * $moreThanOne - */ -final case class ReturningWithRegex[+R](result: Match => R) extends AbstractReturningWithRegex[R] { - def run[RR >: R](when: RegexWhen[RR], runContext: RunContext[RR]): RunContext[RR] = { - // The value in RunContext is () => R because: - // 1) We want it to be lazy so we don't trigger an evaluation of the defaultValue when we first create the RunContext. - // 2) It cannot be a call-by-name because we want RunContext to be a case class, and val parameters cannot be by-name. - // However we still want to run the ReturningWithRegex, which means this method cannot be implemented with: - // runContext.copy(value = () => result(when.regexMatch(runContext.output))) - // Because if it were the result function would never be run and the test (in ReturningSpec) - // "An Expect" should "only return the last returning action before an exit but still execute the previous actions" - // Would fail. - - val res = result(when.regexMatch(runContext.output)) - runContext.copy(value = res) - } - - def map[T](f: R => T): AbstractReturningWithRegex[T] = { - this.copy(result andThen f) - } - def flatMap[T](f: R => Expect[T]): AbstractReturningWithRegex[T] = { - ReturningExpectWithRegex(result andThen f) - } - def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): AbstractReturningWithRegex[T] = { - val computeAction: R => AbstractReturningWithRegex[T] = { - //We cannot invoke map/flatMap, because if we did the returning result would be ran twice in the ActionReturningAction: - //Once inside the execute which invokes parent.result - //And another when the action returned by the ActionReturningAction is ran - case r if flatMapPF.isDefinedAt(r) => ReturningExpectWithRegex(_ => flatMapPF(r)) - case r if mapPF.isDefinedAt(r) => this.copy(_ => mapPF(r)) - case r => pfNotDefined[R, AbstractReturningWithRegex[T]](r) - } - ActionReturningActionWithRegex(this, computeAction) - } - - def structurallyEquals[RR >: R, W[+X] <: RegexWhen[X]](other: Action[RR, W]): Boolean = other.isInstanceOf[ReturningWithRegex[RR]] -} - -/** - * When this action is executed: - * - * 1. The current run of Expect is terminated (like with an `Exit`) but its return value is discarded. - * 2. `result` is evaluated to obtain the expect. - * 3. The obtained expect is run. - * 4. The result obtained in the previous step becomes the result of the current expect (the terminated one). - * - * This works out as a special combination of `Exit` and `Returning`. Where the exit deallocates the - * resources allocated by the current expect. And the result of the `Returning` is obtained from the result of - * executing the received expect. - * - * This allows to construct the Expect based on the regex Match. - * $regexWhen - * Any action or expect block added after this will not be executed. - */ -final case class ReturningExpectWithRegex[+R](result: Match => Expect[R]) extends AbstractReturningWithRegex[R] { - def run[RR >: R](when: RegexWhen[RR], runContext: RunContext[RR]): RunContext[RR] = { - val regexMatch = when.regexMatch(runContext.output) - val expect = result(regexMatch) - runContext.copy(executionAction = ChangeToNewExpect(expect)) - } - - def map[T](f: R => T): AbstractReturningWithRegex[T] = { - this.copy(result.andThen(_.map(f))) - } - def flatMap[T](f: R => Expect[T]): AbstractReturningWithRegex[T] = { - this.copy(result.andThen(_.flatMap(f))) - } - def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): AbstractReturningWithRegex[T] = { - this.copy(result.andThen(_.transform(flatMapPF, mapPF))) - } - - def structurallyEquals[RR >: R, W[+X] <: RegexWhen[X]](other: Action[RR, W]): Boolean = other.isInstanceOf[ReturningExpectWithRegex[RR]] -} - -final case class ActionReturningActionWithRegex[R, +T](parent: ReturningWithRegex[R], resultAction: R => AbstractReturningWithRegex[T]) - extends AbstractReturningWithRegex[T] { - - def run[TT >: T](when: RegexWhen[TT], runContext: RunContext[TT]): RunContext[TT] = { - val regexMatch = when.regexMatch(runContext.stdOutOutput) - val parentResult: R = parent.result(regexMatch) - resultAction(parentResult).run(when, runContext) - } - - def map[U](f: T => U): AbstractReturningWithRegex[U] = { - this.copy(parent, resultAction.andThen(_.map(f))) - } - def flatMap[U](f: T => Expect[U]): AbstractReturningWithRegex[U] = { - this.copy(parent, resultAction.andThen(_.flatMap(f))) - } - def transform[U](flatMapPF: T /=> Expect[U], mapPF: T /=> U): AbstractReturningWithRegex[U] = { - this.copy(parent, resultAction.andThen(_.transform(flatMapPF, mapPF))) - } - - def structurallyEquals[TT >: T, W[+X] <: RegexWhen[X]](other: Action[TT, W]): Boolean = other.isInstanceOf[ActionReturningActionWithRegex[R, TT]] -} \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/actions/Send.scala b/src/main/scala/work/martins/simon/expect/core/actions/Send.scala index aa448cb..e206c39 100644 --- a/src/main/scala/work/martins/simon/expect/core/actions/Send.scala +++ b/src/main/scala/work/martins/simon/expect/core/actions/Send.scala @@ -1,39 +1,44 @@ package work.martins.simon.expect.core.actions -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect.core.{Expect, RunContext, When} -import scala.language.higherKinds import scala.util.matching.Regex.Match +import work.martins.simon.expect./=> +import work.martins.simon.expect.StringUtils.* +import work.martins.simon.expect.core.{Expect, RunContext, When, RegexWhen} -object Sendln { - def apply[R](text: String, sensitive: Boolean = false): Send[R] = Send(text + System.lineSeparator(), sensitive) - def apply[R](text: Match => String): SendWithRegex[R] = SendWithRegex(text.andThen(_ + System.lineSeparator())) -} +object Sendln: + def apply(text: String, sensitive: Boolean = false): Send = Send(text + System.lineSeparator(), sensitive) + def apply(text: Match => String): SendWithRegex = SendWithRegex(text.andThen(_ + System.lineSeparator())) -object Send { - def apply[R](text: String, sensitive: Boolean = false): Send[R] = new Send(text, sensitive) - def apply[R](text: Match => String): SendWithRegex[R] = SendWithRegex(text) -} +object Send: + def apply(text: String, sensitive: Boolean = false): Send = new Send(text, sensitive) + def apply(text: Match => String): SendWithRegex = SendWithRegex(text) /** * When this action is executed `text` will be sent to the stdIn of the underlying process. * * @param text the text to send. + * @param sensitive whether to ommit `text` in `toString` */ -final case class Send[+R](text: String, sensitive: Boolean = false) extends Action[R, When] { - def run[RR >: R](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = { +final case class Send(text: String, sensitive: Boolean = false) extends NonProducingAction[When]: + def run[RR >: Nothing](when: When[RR], runContext: RunContext[RR]): RunContext[RR] = runContext.process.write(text) runContext - } + + def structurallyEquals[RR >: Nothing](other: Action[RR, ?]): Boolean = other.isInstanceOf[Send] + + override def toString: String = s"Send(${if sensitive then "" else escape(text) })" - //These methods just perform a cast because the type argument R is just used here, - //so there isn't the need to allocate need objects. - - def map[T](f: R => T): Action[T, When] = this.asInstanceOf[Send[T]] - def flatMap[T](f: R => Expect[T]): Action[T, When] = this.asInstanceOf[Send[T]] - def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): Action[T, When] = this.asInstanceOf[Send[T]] - - def structurallyEquals[RR >: R, W[+X] <: When[X]](other: Action[RR, W]): Boolean = other.isInstanceOf[Send[RR]] - - override def toString: String = s"${this.getClass.getSimpleName}(${if (sensitive) "" else escape(text) })" -} +/** + * When this action is executed the result of evaluating `text` will be sent to the stdIn of the underlying process. + * This allows to send data to the process based on the regex Match. + * $regexWhen + * + * @param text the text to send. + */ +final case class SendWithRegex(text: Match => String) extends NonProducingAction[RegexWhen]: + def run[RR >: Nothing](when: RegexWhen[RR], runContext: RunContext[RR]): RunContext[RR] = + val regexMatch = when.regexMatch(runContext.output) + runContext.process.write(text(regexMatch)) + runContext + + def structurallyEquals[RR >: Nothing](other: Action[RR, ?]): Boolean = other.isInstanceOf[SendWithRegex] \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/core/actions/SendWithRegex.scala b/src/main/scala/work/martins/simon/expect/core/actions/SendWithRegex.scala deleted file mode 100644 index 1995e20..0000000 --- a/src/main/scala/work/martins/simon/expect/core/actions/SendWithRegex.scala +++ /dev/null @@ -1,30 +0,0 @@ -package work.martins.simon.expect.core.actions - -import work.martins.simon.expect.core._ - -import scala.language.higherKinds -import scala.util.matching.Regex.Match - -/** - * When this action is executed the result of evaluating `text` will be sent to the stdIn of the underlying process. - * This allows to send data to the process based on the regex Match. - * $regexWhen - * - * @param text the text to send. - */ -final case class SendWithRegex[+R](text: Match => String) extends Action[R, RegexWhen] { - def run[RR >: R](when: RegexWhen[RR], runContext: RunContext[RR]): RunContext[RR] = { - val regexMatch = when.regexMatch(runContext.output) - runContext.process.write(text(regexMatch)) - runContext - } - - //These methods just perform a cast because the type argument R is just used here, - //so there isn't the need to allocate need objects. - - def map[T](f: R => T): Action[T, RegexWhen] = this.asInstanceOf[SendWithRegex[T]] - def flatMap[T](f: R => Expect[T]): Action[T, RegexWhen] = this.asInstanceOf[SendWithRegex[T]] - def transform[T](flatMapPF: R /=> Expect[T], mapPF: R /=> T): Action[T, RegexWhen] = this.asInstanceOf[SendWithRegex[T]] - - def structurallyEquals[RR >: R, W[+X] <: RegexWhen[X]](other: Action[RR, W]): Boolean = other.isInstanceOf[SendWithRegex[RR]] -} diff --git a/src/main/scala/work/martins/simon/expect/dsl/Expect.scala b/src/main/scala/work/martins/simon/expect/dsl/Expect.scala index fad5702..88433e9 100644 --- a/src/main/scala/work/martins/simon/expect/dsl/Expect.scala +++ b/src/main/scala/work/martins/simon/expect/dsl/Expect.scala @@ -1,88 +1,62 @@ package work.martins.simon.expect.dsl -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect._ - +import scala.util.NotGiven import scala.util.matching.Regex import scala.util.matching.Regex.Match +import work.martins.simon.expect.* +import work.martins.simon.expect.fluent.{Expect as _, *} -case class Expect[R](command: Seq[String], defaultValue: R, settings: Settings = Settings.fromConfig()) { - def this(command: String, defaultValue: R, settings: Settings) = { - this(splitBySpaces(command), defaultValue, settings) - } - def this(command: String, defaultValue: R) = { - this(command, defaultValue, new Settings()) - } - require(command.nonEmpty, "Expect must have a command to run.") - - private[expect] val fluentExpect = fluent.Expect(command, defaultValue, settings) +//Useful conversion to use in returningExpect actions, which are waiting to receive a core.Expect +given dslToCoreExpect[R]: Conversion[Expect[R], core.Expect[R]] = _.toCore - protected var expectBlock: Option[fluent.ExpectBlock[R]] = None - protected var when: Option[fluent.When[R]] = None +open case class Expect[R](command: Seq[String] | String, defaultValue: R, settings: Settings = Settings.fromConfig()) derives CanEqual: + private val fluentExpect = fluent.Expect(command, defaultValue, settings) - private def newExpect(block: fluent.Expect[R] => fluent.ExpectBlock[R])(f: => Unit): Unit = { - require(expectBlock.isEmpty && when.isEmpty, "Expect block must be the top level object.") - val createdBlock = block(fluentExpect) - expectBlock = Some(createdBlock) - f - expectBlock = None - require(createdBlock.containsWhens(), "Expect block cannot be empty.") - } - def expect(f: => Unit): Unit = newExpect(_.expect)(f) - def addExpectBlock(block: Expect[R] => Unit): Unit = block(this) - // TODO create scalafix rules to migrate the expect shortcuts to the new code + def expectBlock(whens: ExpectBlock[R] ?=> Unit)(using NotGiven[ExpectBlock[R]]): ExpectBlock[R] = + // NotGiven prevents expectBlocks from being created inside expectBlocks + given expectBlock: ExpectBlock[R] = fluentExpect.expectBlock + whens + require(expectBlock.containsWhens(), "Expect block cannot be empty.") + expectBlock + def addExpectBlock(f: Expect[R] => Unit): Unit = f(this) + + private def newWhen[W[X] <: When[X]](f: ExpectBlock[R] => W[R])(actions: W[R] ?=> Unit)(using expectBlock: ExpectBlock[R]): W[R] = + given when: W[R] = f(expectBlock) + actions + when + def when(pattern: String)(actions: StringWhen[R] ?=> Unit)(using ExpectBlock[R]): StringWhen[R] = + newWhen(_.when(pattern))(actions) + def when(pattern: String, readFrom: FromInputStream)(actions: StringWhen[R] ?=> Unit)(using ExpectBlock[R]): StringWhen[R] = + newWhen(_.when(pattern, readFrom))(actions) + def when(pattern: Regex)(actions: RegexWhen[R] ?=> Unit)(using ExpectBlock[R]): RegexWhen[R] = + newWhen(_.when(pattern))(actions) + def when(pattern: Regex, readFrom: FromInputStream)(actions: RegexWhen[R] ?=> Unit)(using ExpectBlock[R]): RegexWhen[R] = + newWhen(_.when(pattern, readFrom))(actions) + def when(pattern: EndOfFile.type)(actions: EndOfFileWhen[R] ?=> Unit)(using ExpectBlock[R]): EndOfFileWhen[R] = + newWhen(_.when(pattern))(actions) + def when(pattern: EndOfFile.type, readFrom: FromInputStream)(actions: EndOfFileWhen[R] ?=> Unit)(using ExpectBlock[R]): EndOfFileWhen[R] = + newWhen(_.when(pattern, readFrom))(actions) + def when(pattern: Timeout.type)(actions: TimeoutWhen[R] ?=> Unit)(using ExpectBlock[R]): TimeoutWhen[R] = + newWhen(_.when(pattern))(actions) + def addWhen(f: Expect[R] => Unit)(using ExpectBlock[R]): Unit = f(this) + def addWhens(f: Expect[R] => Unit)(using ExpectBlock[R]): Unit = f(this) + + def send(text: String, sensitive: Boolean = false)(using when: When[R]): Unit = when.send(text, sensitive) + def send(text: Match => String)(using when: RegexWhen[R]): Unit = when.send(text) + def sendln(text: String, sensitive: Boolean = false)(using when: When[R]): Unit = when.sendln(text, sensitive) + def sendln(text: Match => String)(using when: RegexWhen[R]): Unit = when.sendln(text) + def returning(result: => R)(using when: When[R]): Unit = when.returning(result) + def returning(result: Match => R)(using when: RegexWhen[R]): Unit = when.returning(result) + def returningExpect(result: => core.Expect[R])(using when: When[R]): Unit = when.returningExpect(result) + def returningExpect(result: Match => core.Expect[R])(using when: RegexWhen[R]): Unit = when.returningExpect(result) + def exit()(using when: When[R]): Unit = when.exit() + def addActions[W[X] <: When[X]](f: Expect[R] => Unit)(using W[R]): Unit = f(this) - private def newWhen[W <: fluent.When[R]](block: fluent.ExpectBlock[R] => W)(f: => Unit): Unit = { - require(expectBlock.isDefined && when.isEmpty, "When can only be added inside an Expect Block.") - expectBlock.foreach { eb => - when = Some(block(eb)) - f - when = None - } - } - def when(pattern: String)(f: => Unit): Unit = newWhen(_.when(pattern))(f) - def when(pattern: String, readFrom: FromInputStream)(f: => Unit): Unit = newWhen(_.when(pattern, readFrom))(f) - def when(pattern: Regex)(f: => Unit): Unit = newWhen(_.when(pattern))(f) - def when(pattern: Regex, readFrom: FromInputStream)(f: => Unit): Unit = newWhen(_.when(pattern, readFrom))(f) - def when(pattern: EndOfFile.type)(f: => Unit): Unit = newWhen(_.when(pattern))(f) - def when(pattern: EndOfFile.type, readFrom: FromInputStream)(f: => Unit): Unit = newWhen(_.when(pattern, readFrom))(f) - def when(pattern: Timeout.type)(f: => Unit): Unit = newWhen(_.when(pattern))(f) - def addWhen(block: Expect[R] => Unit): Unit = block(this) - def addWhens(block: Expect[R] => Unit): Unit = block(this) - - private def newAction(block: fluent.When[R] => fluent.When[R]): Unit = { - require(expectBlock.isDefined && when.isDefined, "An Action can only be added inside a When.") - when.foreach { w => - block(w) - () - } - } - private def newRegexAction(block: fluent.RegexWhen[R] => fluent.RegexWhen[R]): Unit = { - val regexWhen: Option[fluent.RegexWhen[R]] = when.collect { case r: fluent.RegexWhen[R] => r } - require(expectBlock.isDefined && when.isDefined, "An Action can only be added inside a When.") - require(regexWhen.isDefined, "This action can only be invoked for RegexWhen") - regexWhen.foreach { w => - block(w) - () - } - } - def send(text: String, sensitive: Boolean = false): Unit = newAction(_.send(text, sensitive)) - def send(text: Match => String): Unit = newRegexAction(_.send(text)) - def sendln(text: String, sensitive: Boolean = false): Unit = newAction(_.sendln(text, sensitive)) - def sendln(text: Match => String): Unit = newRegexAction(_.sendln(text)) - def returning(result: => R): Unit = newAction(_.returning(result)) - def returning(result: Match => R): Unit = newRegexAction(_.returning(result)) - def returningExpect(result: => core.Expect[R]): Unit = newAction(_.returningExpect(result)) - def returningExpect(result: Match => core.Expect[R]): Unit = newRegexAction(_.returningExpect(result)) - def exit(): Unit = newAction(_.exit()) - def addActions(block: Expect[R] => Unit): Unit = block(this) - def toCore: core.Expect[R] = fluentExpect.toCore - + override def toString: String = fluentExpect.toString - override def equals(other: Any): Boolean = other match { - case that: Expect[R] => fluentExpect.equals(that.fluentExpect) + + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + case that: Expect[?] => fluentExpect.equals(that.fluentExpect) case _ => false - } - override def hashCode(): Int = fluentExpect.hashCode() -} + override def hashCode(): Int = fluentExpect.hashCode() \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/dsl/package.scala b/src/main/scala/work/martins/simon/expect/dsl/package.scala deleted file mode 100644 index d630b7e..0000000 --- a/src/main/scala/work/martins/simon/expect/dsl/package.scala +++ /dev/null @@ -1,6 +0,0 @@ -package work.martins.simon.expect - -package object dsl { - //Useful conversion to use in returningExpect actions, which are waiting to receive a core.Expect - implicit def dslToCoreExpect[R](expect: Expect[R]): core.Expect[R] = expect.toCore -} diff --git a/src/main/scala/work/martins/simon/expect/fluent/Expect.scala b/src/main/scala/work/martins/simon/expect/fluent/Expect.scala index bed5ad3..f802072 100644 --- a/src/main/scala/work/martins/simon/expect/fluent/Expect.scala +++ b/src/main/scala/work/martins/simon/expect/fluent/Expect.scala @@ -1,49 +1,36 @@ package work.martins.simon.expect.fluent -import work.martins.simon.expect.StringUtils._ +import work.martins.simon.expect.StringUtils.* import work.martins.simon.expect.{Settings, core} -/** - * @define type Expect - */ -case class Expect[R](command: Seq[String], defaultValue: R, settings: Settings = Settings.fromConfig()) extends Expectable[R] { - def this(command: String, defaultValue: R, settings: Settings) = this(splitBySpaces(command), defaultValue, settings) - def this(command: String, defaultValue: R) = this(command, defaultValue, Settings.fromConfig()) - - require(command.nonEmpty, "Expect must have a command to run.") +//Useful conversion to use in returningExpect actions, which are waiting to receive a core.Expect +given fluentToCoreExpect[R]: Conversion[Expect[R], core.Expect[R]] = _.toCore +open case class Expect[R](command: Seq[String] | String, defaultValue: R, settings: Settings = Settings.fromConfig()) extends Expectable[R]: + val commandSeq: Seq[String] = properCommand(command) + require(commandSeq.nonEmpty, "Expect must have a command to run.") + protected val expectableParent: Expect[R] = this protected var expectBlocks = Seq.empty[ExpectBlock[R]] - override def expect: ExpectBlock[R] = { + override def expectBlock: ExpectBlock[R] = val block = ExpectBlock[R](this) expectBlocks :+= block block - } - - override def addExpectBlock(f: Expect[R] => ExpectBlock[R]): Expect[R] = { + + override def addExpectBlock(f: Expect[R] => ExpectBlock[R]): Expect[R] = f(this) this - } - - /** - * @return the core.Expect equivalent of this fluent.Expect. - */ - def toCore: core.Expect[R] = new core.Expect[R](command, defaultValue, settings)(expectBlocks.map(_.toCore):_*) - + + /** @return the core Expect equivalent of this Expect. */ + def toCore: core.Expect[R] = new core.Expect[R](command, defaultValue, settings)(expectBlocks.map(_.toCore)*) + override def toString: String = s"""Expect: |\tCommand: $command |\tDefaultValue: $defaultValue |\tSettings: $settings - |${expectBlocks.mkString("\n").indent()} - """.stripMargin - override def equals(other: Any): Boolean = other match { - case that: Expect[R] => - command == that.command && - defaultValue == that.defaultValue && - settings == that.settings && - expectBlocks == that.expectBlocks + |${expectBlocks.mkString("\n").indent()}""".stripMargin + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + case e @ Expect(`command`, `defaultValue`, `settings`) if e.expectBlocks == expectBlocks => true case _ => false - } - override def hashCode(): Int = Seq(command, defaultValue, settings, expectBlocks).map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) -} + override def hashCode(): Int = Seq(command, defaultValue, settings, expectBlocks).map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/fluent/ExpectBlock.scala b/src/main/scala/work/martins/simon/expect/fluent/ExpectBlock.scala index 86854f3..bd8d02b 100644 --- a/src/main/scala/work/martins/simon/expect/fluent/ExpectBlock.scala +++ b/src/main/scala/work/martins/simon/expect/fluent/ExpectBlock.scala @@ -1,47 +1,42 @@ package work.martins.simon.expect.fluent -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect._ - import scala.util.matching.Regex +import work.martins.simon.expect.* +import work.martins.simon.expect.StringUtils.* +import work.martins.simon.expect.FromInputStream.StdOut -/** - * @define type ExpectBlock - */ -final case class ExpectBlock[R](parent: Expect[R]) extends Whenable[R] { +final case class ExpectBlock[R](parent: Expect[R]) extends Whenable[R]: + // Unfortunately we cannot enforce a fluent ExpectBlock to have whens. + protected val whenableParent: ExpectBlock[R] = this - + protected var whens = Seq.empty[When[R]] - protected def newWhen[W <: When[R]](when: W): W = { + protected def newWhen[W <: When[R]](when: W): W = whens :+= when when - } - override def when(pattern: String, readFrom: FromInputStream): StringWhen[R] = newWhen(StringWhen(this)(pattern, readFrom)) - override def when(pattern: Regex, readFrom: FromInputStream): RegexWhen[R] = newWhen(RegexWhen(this)(pattern, readFrom)) - override def when(pattern: EndOfFile.type, readFrom: FromInputStream): EndOfFileWhen[R] = newWhen(EndOfFileWhen(this)(readFrom)) + override def when(pattern: String): StringWhen[R] = newWhen(StringWhen(this, pattern)) + override def when(pattern: String, readFrom: FromInputStream): StringWhen[R] = newWhen(StringWhen(this, pattern, readFrom)) + override def when(pattern: Regex): RegexWhen[R] = newWhen(RegexWhen(this, pattern)) + override def when(pattern: Regex, readFrom: FromInputStream): RegexWhen[R] = newWhen(RegexWhen(this, pattern, readFrom)) + override def when(pattern: EndOfFile.type, readFrom: FromInputStream = StdOut): EndOfFileWhen[R] = newWhen(EndOfFileWhen(this, readFrom)) override def when(pattern: Timeout.type): TimeoutWhen[R] = newWhen(TimeoutWhen(this)) override def addWhen[W <: When[R]](f: ExpectBlock[R] => W): W = f(this) - override def addWhens(f: ExpectBlock[R] => When[R]): ExpectBlock[R] = { + override def addWhens(f: ExpectBlock[R] => When[R]): ExpectBlock[R] = f(this) this - } - - protected[expect] def containsWhens(): Boolean = whens.nonEmpty - - /*** - * @return the core.ExpectBlock equivalent of this fluent.ExpectBlock. - */ - def toCore: core.ExpectBlock[R] = core.ExpectBlock(whens.map(_.toCore):_*) - - override def toString: String = { + + def containsWhens(): Boolean = whens.nonEmpty + + /** @return the core ExpectBlock equivalent of this ExpectBlock. */ + def toCore: core.ExpectBlock[R] = core.ExpectBlock(whens.map(_.toCore)*) + + override def toString: String = s"""expect { |${whens.mkString("\n").indent()} |}""".stripMargin - } - override def equals(other: Any): Boolean = other match { - case that: ExpectBlock[R] => whens == that.whens + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + case that: ExpectBlock[?] => whens == that.whens case _ => false - } - override def hashCode(): Int = whens.hashCode() -} + + override def hashCode(): Int = whens.hashCode() \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/fluent/Expectable.scala b/src/main/scala/work/martins/simon/expect/fluent/Expectable.scala index 4ab03b8..6e396d2 100644 --- a/src/main/scala/work/martins/simon/expect/fluent/Expectable.scala +++ b/src/main/scala/work/martins/simon/expect/fluent/Expectable.scala @@ -1,28 +1,27 @@ package work.martins.simon.expect.fluent -trait Expectable[R] { - protected val expectableParent: Expect[R] //The root of an Expectable must be an Expect - +trait Expectable[R]: + protected def expectableParent: Expect[R] //The root of an Expectable must be an Expect + /** * Adds a new `ExpectBlock`. * @return the new `ExpectBlock`. */ - def expect: ExpectBlock[R] = expectableParent.expect - + def expectBlock: ExpectBlock[R] = expectableParent.expectBlock + /** * Add arbitrary `ExpectBlock`s to this `Expect`. * * This is helpful to refactor code. For example: imagine you have an error case you want to add to multiple expects. * You could leverage this method to do so in the following way: * {{{ - * def errorCaseExpectBlock(e: Expect[String]): Unit { + * def errorCaseExpectBlock(e: Expect[String]): Unit = * e.expect * .when("Some error") * .returning("Got some error") - * } * * //Then in your expects - * def parseOutputA: Expect[String] = { + * def parseOutputA: Expect[String] = * val e = new Expect("some command", "") * e.expect(...) * e.expect @@ -31,9 +30,8 @@ trait Expectable[R] { * .when(...) * .action2 * e.addExpectBlock(errorCaseExpectBlock) - * } * - * def parseOutputB: Expect[String] = { + * def parseOutputB: Expect[String] = * val e = new Expect("some command", "") * e.expect * .when(...) @@ -44,13 +42,9 @@ trait Expectable[R] { * e.expect(...) * .action2 * e.addExpectBlock(errorCaseExpectBlock) - * } * }}} * * @param f function that adds `ExpectBlock`s. * @return this `Expect`. */ - def addExpectBlock(f: Expect[R] => ExpectBlock[R]): Expect[R] = expectableParent.addExpectBlock(f) - - // TODO create scalafix rules to migrate the expect shortcuts to the new code -} + def addExpectBlock(f: Expect[R] => ExpectBlock[R]): Expect[R] = expectableParent.addExpectBlock(f) \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/fluent/Whenable.scala b/src/main/scala/work/martins/simon/expect/fluent/Whenable.scala index d0b7c9d..bac6b46 100644 --- a/src/main/scala/work/martins/simon/expect/fluent/Whenable.scala +++ b/src/main/scala/work/martins/simon/expect/fluent/Whenable.scala @@ -1,12 +1,13 @@ package work.martins.simon.expect.fluent -import work.martins.simon.expect.{EndOfFile, FromInputStream, StdOut, Timeout} +import work.martins.simon.expect.{EndOfFile, FromInputStream, Timeout} +import work.martins.simon.expect.FromInputStream.StdOut import scala.util.matching.Regex -trait Whenable[R] extends Expectable[R] { - protected implicit val whenableParent: ExpectBlock[R] //The root of an Whenable must be an ExpectBlock - protected lazy val expectableParent: Expect[R] = whenableParent.parent - +trait Whenable[R] extends Expectable[R]: + protected def whenableParent: ExpectBlock[R] //The root of an Whenable must be an ExpectBlock + protected def expectableParent: Expect[R] = whenableParent.parent + /** * Adds a new `StringWhen` that matches whenever `pattern` is contained * in the text read from the `FromInputStream` specified in the parent ExpectBlock. @@ -22,7 +23,7 @@ trait Whenable[R] extends Expectable[R] { * @return the new `StringWhen`. */ def when(pattern: String, readFrom: FromInputStream): StringWhen[R] = whenableParent.when(pattern, readFrom) - + /** * Adds a new `RegexWhen` that matches whenever the regex `pattern` successfully matches * against the text read from `FromInputStream` specified in the parent ExpectBlock. @@ -38,7 +39,7 @@ trait Whenable[R] extends Expectable[R] { * @return the new `RegexWhen`. */ def when(pattern: Regex, readFrom: FromInputStream): RegexWhen[R] = whenableParent.when(pattern, readFrom) - + /** * Adds a new `EndOfFileWhen` that matches whenever the EndOfFile in read from `FromInputStream` * specified in the parent ExpectBlock. @@ -53,27 +54,26 @@ trait Whenable[R] extends Expectable[R] { * @return the new `EndOfFileWhen`. */ def when(pattern: EndOfFile.type, readFrom: FromInputStream): EndOfFileWhen[R] = whenableParent.when(pattern, readFrom) - + /** * Adds a new `TimeoutWhen` that matches whenever the read from any of the `FromStreamInput`s times out. * @param pattern the pattern to match against. * @return the new `TimeoutWhen`. */ def when(pattern: Timeout.type): TimeoutWhen[R] = whenableParent.when(pattern) - + /** * Add an arbitrary `When` to this `ExpectBlock`. * * This is helpful to refactor code. For example: imagine you have an error case you want to add to * multiple `ExpectBlock`s. You could leverage this method to do so in the following way: * {{{ - * def errorCaseWhen(expectBlock: ExpectBlock[String]): When[String] = { + * def errorCaseWhen(expectBlock: ExpectBlock[String]): When[String] = * expectBlock * .when("Some error") * .returning("Got some error") - * } * - * def parseOutputA: Expect[String] = { + * def parseOutputA: Expect[String] = * val e = new Expect("some command", "") * e.expect * .when(...) @@ -81,16 +81,14 @@ trait Whenable[R] extends Expectable[R] { * e.expect * .addWhen(errorCaseWhen) * .exit() - * } * - * def parseOutputB: Expect[String] = { + * def parseOutputB: Expect[String] = * val e = new Expect("some command", "") * e.expect * .when(...) * .sendln(..) * .returning(...) * .addWhen(errorCaseWhen) - * } * }}} * * This function returns the added When which allows you to add further actions, see the exit action of the @@ -132,5 +130,4 @@ trait Whenable[R] extends Expectable[R] { * @param f function that adds `When`s. * @return this ExpectBlock. */ - def addWhens(f: ExpectBlock[R] => When[R]): ExpectBlock[R] = whenableParent.addWhens(f) -} + def addWhens(f: ExpectBlock[R] => When[R]): ExpectBlock[R] = whenableParent.addWhens(f) \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/fluent/Whens.scala b/src/main/scala/work/martins/simon/expect/fluent/Whens.scala index 6da0589..f4b08d7 100644 --- a/src/main/scala/work/martins/simon/expect/fluent/Whens.scala +++ b/src/main/scala/work/martins/simon/expect/fluent/Whens.scala @@ -4,33 +4,29 @@ import scala.language.higherKinds import scala.util.matching.Regex import scala.util.matching.Regex.Match -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect.core.actions._ -import work.martins.simon.expect.{FromInputStream, StdOut, core} - -/** - * @define type When - */ -sealed trait When[R] extends Whenable[R] { - /** The concrete core.When type constructor which this fluent.When is a builder for. */ - type CW[+X] <: core.When[X] - +import work.martins.simon.expect.StringUtils.* +import work.martins.simon.expect.core.actions.* +import work.martins.simon.expect.{FromInputStream, core} +import work.martins.simon.expect.FromInputStream.StdOut + +sealed trait When[R] extends Whenable[R]: + /** The concrete core When type constructor which this When is a builder for. */ + type CoreWhen[+X] <: core.When[X] + /** The concrete When type constructor to which the actions will be applied. */ type This[X] <: When[X] - val thisSubtype: This[R] = this.asInstanceOf[This[R]] def readFrom: FromInputStream - def parent: ExpectBlock[R] - + val parent: ExpectBlock[R] + protected val whenableParent: ExpectBlock[R] = parent - - protected var actions = Seq.empty[Action[R, CW]] - protected def newAction(action: Action[R, CW]): this.type = { + + protected var actions = Seq.empty[Action[R, CoreWhen]] + protected def newAction(action: Action[R, CoreWhen]): this.type = actions :+= action this - } - + /** * Send `text` to the stdIn of the underlying process. * Send will only occur when Expect is run. @@ -39,101 +35,92 @@ sealed trait When[R] extends Whenable[R] { */ def send(text: String, sensitive: Boolean = false): this.type = newAction(Send(text, sensitive)) /** - * Sends `text` terminated with `System.lineSeparator()` to the stdIn of the underlying process. - * Send will only occur when Expect is run. + * Sends `text` terminated with `System.lineSeparator()` to the stdIn of the underlying process. + * Send will only occur when Expect is run. * * @return this When. - */ + */ def sendln(text: String, sensitive: Boolean = false): this.type = newAction(Sendln(text, sensitive)) /** - * Returns `result` when this Expect is run. - * If this method is invoked more than once only the last `result` will be returned. - * Note however that the previous returning actions will also be executed. + * Returns `result` when this Expect is run. + * If this method is invoked more than once only the last `result` will be returned. + * Note however that the previous returning actions will also be executed. * * @return this When. - */ + */ def returning(result: => R): this.type = newAction(Returning(result)) - + def returningExpect(result: => core.Expect[R]): this.type = newAction(ReturningExpect(result)) - - /** - * Add arbitrary `Action`s to this `When`. - * - * This is helpful to refactor code. For example: imagine you want to perform the same actions whenever an error - * occurs. You could leverage this method to do so in the following way: - * {{{ - * def preemtiveExit(when: When[String]): Unit { - * when - * .returning("Got some error") - * .exit() - * } - * - * def parseOutputA: Expect[String] = { - * val e = new Expect("some command", "") - * e.expect - * .when(...) - * .action1 - * .when(...) - * .addActions(preemtiveExit) - * } - * - * def parseOutputB: Expect[String] = { - * val e = new Expect("some command", "") - * e.expect(...) - * .addActions(preemtiveExit) - * } - * }}} - * - * @param f function that adds `Action`s. - * @return this `When`. - */ - def addActions(f: This[R] => When[R]): this.type = { - f(thisSubtype) - this - } - + /** - * Terminates the current run of Expect causing it to return the last returned value. - * Any action or expect block added after this Exit will not be executed. + * Terminates the current run of Expect causing it to return the last returned value. + * Any action or expect block added after this Exit will not be executed. * * @return this When. - */ + */ def exit(): this.type = newAction(Exit()) - + + /** + * Add arbitrary `Action`s to this `When`. + * + * This is helpful to refactor code. For example: imagine you want to perform the same actions whenever an error + * occurs. You could leverage this method to do so in the following way: + * {{{ + * def preemtiveExit(when: When[String]): Unit = + * when + * .returning("Got some error") + * .exit() + * + * def parseOutputA: Expect[String] = + * val e = new Expect("some command", "") + * e.expect + * .when(...) + * .action1 + * .when(...) + * .addActions(preemtiveExit) + * + * def parseOutputB: Expect[String] = + * val e = new Expect("some command", "") + * e.expect(...) + * .addActions(preemtiveExit) + * }}} + * + * @param f function that adds `Action`s. + * @return this `When`. + */ + def addActions(f: This[R] => When[R]): this.type = + f(this.asInstanceOf[This[R]]) + this + /** * @return the core.When equivalent of this fluent.When. */ - def toCore: CW[R] - + def toCore: CoreWhen[R] + def toString(pattern: String): String = s"""when($pattern, readFrom = $readFrom) { |${actions.mkString("\n").indent()} |}""".stripMargin -} - -// TODO: the toString, equals and hashCode have exactly the same implementation as in the core.Whens. Can we refactor it? -case class StringWhen[R](parent: ExpectBlock[R])(val pattern: String, val readFrom: FromInputStream = StdOut) extends When[R] { - type CW[+X] = core.StringWhen[X] +case class StringWhen[R](parent: ExpectBlock[R], pattern: String, readFrom: FromInputStream = StdOut) extends When[R]: + type CoreWhen[+X] = core.StringWhen[X] type This[X] = StringWhen[X] - - def toCore: core.StringWhen[R] = new core.StringWhen[R](pattern, readFrom)(actions:_*) - + + def toCore: core.StringWhen[R] = core.StringWhen[R](pattern, readFrom, actions*) + override def toString: String = toString(escape(pattern)) - override def equals(other: Any): Boolean = other match { - case that: StringWhen[R] => pattern == that.pattern && readFrom == that.readFrom && actions == that.actions + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + case that: StringWhen[?] => pattern == that.pattern && readFrom == that.readFrom && actions == that.actions case _ => false - } - override def hashCode(): Int = { + override def hashCode(): Int = Seq(pattern, readFrom, actions) .map(_.hashCode()) .foldLeft(0)((a, b) => 31 * a + b) - } -} -case class RegexWhen[R](parent: ExpectBlock[R])(val pattern: Regex, val readFrom: FromInputStream = StdOut) extends When[R] { - type CW[+X] = core.RegexWhen[X] - type This[X] = RegexWhen[X] +case class RegexWhen[R](parent: ExpectBlock[R], pattern: Regex, readFrom: FromInputStream = StdOut) extends When[R]: + type CoreWhen[+X] = core.RegexWhen[X] + type This[X] = RegexWhen[X] + /** * Send the result of invoking `text` with the `Match` of the regex used, to the stdIn of the underlying process. * Send will only occur when Expect is run. @@ -157,55 +144,49 @@ case class RegexWhen[R](parent: ExpectBlock[R])(val pattern: Regex, val readFrom * @return this When. */ def returning(result: Match => R): RegexWhen[R] = newAction(ReturningWithRegex(result)) - + def returningExpect(result: Match => core.Expect[R]): RegexWhen[R] = newAction(ReturningExpect(result)) - - def toCore: core.RegexWhen[R] = new core.RegexWhen[R](pattern, readFrom)(actions:_*) - + + def toCore: core.RegexWhen[R] = core.RegexWhen[R](pattern, readFrom, actions*) + override def toString: String = toString(escape(pattern.regex) + ".r") - override def equals(other: Any): Boolean = other match { - case that: RegexWhen[R] => pattern.regex == that.pattern.regex && readFrom == that.readFrom && actions == that.actions + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + case that: RegexWhen[?] => pattern.regex == that.pattern.regex && readFrom == that.readFrom && actions == that.actions case _ => false - } - override def hashCode(): Int = { + override def hashCode(): Int = Seq(pattern, readFrom, actions) .map(_.hashCode()) .foldLeft(0)((a, b) => 31 * a + b) - } -} -case class EndOfFileWhen[R](parent: ExpectBlock[R])(val readFrom: FromInputStream = StdOut) extends When[R] { - type CW[+X] = core.EndOfFileWhen[X] - type This[X] = EndOfFileWhen[X] - - def toCore: core.EndOfFileWhen[R] = new core.EndOfFileWhen[R](readFrom)(actions:_*) +case class EndOfFileWhen[R](parent: ExpectBlock[R], readFrom: FromInputStream = StdOut) extends When[R]: + type CoreWhen[+X] = core.EndOfFileWhen[X] + type This[X] = EndOfFileWhen[X] + + def toCore: core.EndOfFileWhen[R] = core.EndOfFileWhen[R](readFrom, actions*) + override def toString: String = toString("EndOfFile") - override def equals(other: Any): Boolean = other match { - case that: EndOfFileWhen[R] => readFrom == that.readFrom && actions == that.actions + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + case that: EndOfFileWhen[?] => readFrom == that.readFrom && actions == that.actions case _ => false - } - override def hashCode(): Int = { + override def hashCode(): Int = Seq(readFrom, actions) .map(_.hashCode()) .foldLeft(0)((a, b) => 31 * a + b) - } -} -case class TimeoutWhen[R](parent: ExpectBlock[R]) extends When[R] { - type CW[+X] = core.TimeoutWhen[X] + +case class TimeoutWhen[R](parent: ExpectBlock[R]) extends When[R]: + type CoreWhen[+X] = core.TimeoutWhen[X] type This[X] = TimeoutWhen[X] - def toCore: core.TimeoutWhen[R] = new core.TimeoutWhen[R]()(actions:_*) + def toCore: core.TimeoutWhen[R] = core.TimeoutWhen[R](actions*) /** The readFrom of a TimeoutWhen is not used but to keep the implementation simple we set its value to StdOut. */ override val readFrom: FromInputStream = StdOut - override def toString: String = s"""when(Timeout) { - |${actions.mkString("\n").indent()} - |}""".stripMargin - override def equals(other: Any): Boolean = other match { - case that: TimeoutWhen[R] => actions == that.actions + override def toString: String = + s"""when(Timeout) { + |${actions.mkString("\n").indent()} + |}""".stripMargin + override def equals(other: Any): Boolean = other.asInstanceOf[Matchable] match + case that: TimeoutWhen[?] => actions == that.actions case _ => false - } - override def hashCode(): Int = actions.hashCode() -} - + override def hashCode(): Int = actions.hashCode() \ No newline at end of file diff --git a/src/main/scala/work/martins/simon/expect/fluent/package.scala b/src/main/scala/work/martins/simon/expect/fluent/package.scala deleted file mode 100644 index 0804c17..0000000 --- a/src/main/scala/work/martins/simon/expect/fluent/package.scala +++ /dev/null @@ -1,6 +0,0 @@ -package work.martins.simon.expect - -package object fluent { - //Useful conversion to use in returningExpect actions, which are waiting to receive a core.Expect - implicit def fluentToCoreExpect[R](expect: Expect[R]): core.Expect[R] = expect.toCore -} diff --git a/src/main/scala/work/martins/simon/expect/package.scala b/src/main/scala/work/martins/simon/expect/package.scala new file mode 100644 index 0000000..9773b9e --- /dev/null +++ b/src/main/scala/work/martins/simon/expect/package.scala @@ -0,0 +1,12 @@ +package work.martins.simon.expect + +import scala.annotation.targetName + +enum FromInputStream derives CanEqual: + case StdOut, StdErr + +object EndOfFile +object Timeout + +@targetName("PartialFunction") +infix type /=>[-A, +B] = PartialFunction[A, B] \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/HashCodeEqualsToStringSpec.scala b/src/test/scala/work/martins/simon/expect/HashCodeEqualsToStringSpec.scala index bd3fa7c..de60e58 100644 --- a/src/test/scala/work/martins/simon/expect/HashCodeEqualsToStringSpec.scala +++ b/src/test/scala/work/martins/simon/expect/HashCodeEqualsToStringSpec.scala @@ -1,15 +1,16 @@ package work.martins.simon.expect -import org.scalatest.{FlatSpec, Matchers} -import work.martins.simon.expect.StringUtils._ -import work.martins.simon.expect.core._ -import work.martins.simon.expect.core.actions.Send -import work.martins.simon.expect.fluent.{Expect, ExpectBlock, When} - import scala.collection.immutable.HashSet import scala.util.Random +import org.scalatest.flatspec.AnyFlatSpecLike +import org.scalatest.matchers.should.* +import work.martins.simon.expect.StringUtils.* +import work.martins.simon.expect.core.* +import work.martins.simon.expect.core.actions.Send +import work.martins.simon.expect.dsl.* +import work.martins.simon.expect.fluent.{Expect, ExpectBlock, When} -class HashCodeEqualsToStringSpec extends FlatSpec with Matchers { +class HashCodeEqualsToStringSpec extends AnyFlatSpecLike with Matchers: def addSendAndExit[R](when: When[R]): When[R] = { when .send("text") @@ -17,7 +18,7 @@ class HashCodeEqualsToStringSpec extends FlatSpec with Matchers { .exit() } def addBlock(e: fluent.Expect[String]): ExpectBlock[String] = { - e.expect + e.expectBlock .addWhens(addWhensEOFAndTimeout) } def addWhensEOFAndTimeout(eb: fluent.ExpectBlock[String]): fluent.TimeoutWhen[String] = { @@ -26,79 +27,79 @@ class HashCodeEqualsToStringSpec extends FlatSpec with Matchers { .when(Timeout) .exit() } - + val objects: Seq[Any] = Seq( Timeout, //The curve ball to test that equals returns false - + //To test equals returns false on expectBlock - core.ExpectBlock(StringWhen("1")()), + core.ExpectBlock(StringWhen("1")), ExpectBlock(new Expect("ls", "")), - + new Expect("ls", ""), - new Expect("ls", "") { - expect + new Expect("tree", "") { + expectBlock .when("1") }, new Expect("ls", "") { - expect + expectBlock .when("2".r) }, new dsl.Expect("ls", "") { - expect{ - when(EndOfFile){} + expectBlock { + when(EndOfFile) {} } }, new Expect("ls", "") { - expect + expectBlock .when(EndOfFile) .when(EndOfFile) .when("") }, new Expect("ls", "") { - expect + expectBlock .when(Timeout) }, new Expect("ls", "") { - expect + expectBlock .when("1") .addActions(addSendAndExit) }, new Expect("ls", "") { - expect + expectBlock .when("2".r) .send("") .exit() }, new Expect("ls", "") { - expect + expectBlock .when(EndOfFile) .addActions(addSendAndExit) }, new Expect("ls", "") { - expect + expectBlock .when(Timeout) .addActions(addSendAndExit) }, new dsl.Expect("ls", "") { - expect { + expectBlock { when("") { send("") } } }, new Expect("ls", "") { - expect + expectBlock .when("a".r) .addActions(addSendAndExit) .addWhens(addWhensEOFAndTimeout) }, new Expect("ls", "") { - expect + expectBlock .when("c".r) .addExpectBlock(addBlock) } ) - + "hashCode and equals" should "work" in { val rnd = new Random() var set = HashSet.empty[Any] //Tests hashCode @@ -106,59 +107,58 @@ class HashCodeEqualsToStringSpec extends FlatSpec with Matchers { val n = rnd.nextInt(5) + 3 //Insert at least 3 set ++= Seq.fill(n)(o) } - + for(o <- objects) { set.count(_ == o) shouldBe 1 //Tests equals } - - val objectsWithCoreExpects = objects map { - case e: Expect[_] => e.toCore - case e: dsl.Expect[_] => e.toCore + + val objectsWithCoreExpects = objects.map(_.asInstanceOf[Matchable] match { + case e: Expect[?] => e.toCore case e => e - } - val setCore = HashSet(objectsWithCoreExpects:_*) //Tests hashCode + }) + val setCore = HashSet(objectsWithCoreExpects*) //Tests hashCode for(o <- objectsWithCoreExpects) { setCore should contain (o) //Tests equals } } - + "toString" should "contain useful information" in { - val expects = objects.collect{ case e: Expect[_] => e } + val expects = objects.collect(_.asInstanceOf[Matchable] match { case e: Expect[?] => e }) for (expect <- expects) { val expectToString = expect.toString expectToString should include ("Expect") expectToString should include (expect.command.toString) expectToString should include (expect.defaultValue.toString) - + val expectCoreToString = expect.toCore.toString expectCoreToString should include ("Expect") expectCoreToString should include (expect.command.toString) expectCoreToString should include (expect.defaultValue.toString) - + val settings = expect.settings val settingsToString = settings.toString settingsToString should include ("Settings") settingsToString should include (settings.timeout.toString) settingsToString should include (settings.charset.toString) - + for (block <- expect.toCore.expectBlocks) { expectToString should include (block.toString.indent()) - + val blockToString = block.toString blockToString should include ("expect") for (when <- block.whens) { blockToString should include (when.toString.indent()) - + val whenToString = when.toString whenToString should include ("when") - + when match { - case StringWhen(pattern, _) => whenToString should include (pattern) - case RegexWhen(pattern, _) => whenToString should include (escape(pattern.regex)) //This one is a little cheat - case EndOfFileWhen(_) => whenToString should include ("EndOfFile") - case TimeoutWhen() => whenToString should include ("Timeout") + case StringWhen(pattern, _, _*) => whenToString should include (pattern) + case RegexWhen(pattern, _, _*) => whenToString should include (escape(pattern.regex)) //This one is a little cheat + case EndOfFileWhen(_, _*) => whenToString should include ("EndOfFile") + case TimeoutWhen(_*) => whenToString should include ("Timeout") } - + for (action <- when.actions) { whenToString should include (action.toString) action.toString should include (action.getClass.getSimpleName) @@ -171,20 +171,19 @@ class HashCodeEqualsToStringSpec extends FlatSpec with Matchers { } } } - + val dslExpect = new dsl.Expect("ls", "") { - expect { + expectBlock { when("") { send("") } } } val fluentExpect = new fluent.Expect("ls", "") { - expect + expectBlock .when("") .send("") } - + dslExpect.toString shouldEqual fluentExpect.toString - } -} + } \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/SettingsSpec.scala b/src/test/scala/work/martins/simon/expect/SettingsSpec.scala index d104983..9d5cb01 100644 --- a/src/test/scala/work/martins/simon/expect/SettingsSpec.scala +++ b/src/test/scala/work/martins/simon/expect/SettingsSpec.scala @@ -1,11 +1,12 @@ package work.martins.simon.expect -import org.scalatest.{Matchers, AsyncWordSpec} -import work.martins.simon.expect.core._ -import work.martins.simon.expect.core.actions._ +import org.scalatest.wordspec.AsyncWordSpec +import org.scalatest.matchers.should.* +import work.martins.simon.expect.core.* +import work.martins.simon.expect.core.actions.* -class SettingsSpec extends AsyncWordSpec with Matchers { +class SettingsSpec extends AsyncWordSpec with Matchers: "Settings" when { "wrong options are specified" should { "throw IllegalArgumentException if timeFactor is < 1" in { @@ -37,10 +38,10 @@ class SettingsSpec extends AsyncWordSpec with Matchers { Settings.fromConfig() shouldBe Settings() } } - + import scala.concurrent.duration.DurationInt val settings = Settings(timeout = 1.minute, timeoutFactor = 1.5) - + "passed to the Expect constructor" should { "be used" in { new Expect(Seq("ls"), (), settings)().settings shouldBe settings @@ -63,7 +64,7 @@ class SettingsSpec extends AsyncWordSpec with Matchers { expect.run().map { _ shouldBe "Found something" } - + // We cannot directly observe whether the settings are in fact being overridden. // So we hide behind causing a timeout to test that they are. expect.run(Settings(timeout = 1.nano)).map { @@ -71,5 +72,4 @@ class SettingsSpec extends AsyncWordSpec with Matchers { } } } - } -} + } \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/TestUtils.scala b/src/test/scala/work/martins/simon/expect/TestUtils.scala index 87541d4..c005e72 100644 --- a/src/test/scala/work/martins/simon/expect/TestUtils.scala +++ b/src/test/scala/work/martins/simon/expect/TestUtils.scala @@ -1,7 +1,8 @@ package work.martins.simon.expect -import org.scalatest.{Assertion, AsyncTestSuite, Matchers} +import org.scalatest.{Assertion, AsyncTestSuite} import org.scalatest.concurrent.ScalaFutures +import org.scalatest.matchers.should.* import work.martins.simon.expect.core.{Expect, ExpectBlock, When} import scala.concurrent.Future @@ -9,10 +10,12 @@ import scala.concurrent.Future trait TestUtils extends ScalaFutures with Matchers { test: AsyncTestSuite => val addedValue = "this is it" def appendToBuilder(builder: StringBuilder): StringBuilder = builder.append(addedValue) - - def constructExpect[R](defaultValue: R, whens: When[R]*): Expect[R] = new Expect("ls", defaultValue)(ExpectBlock(whens:_*)) - def constructExpect(whens: When[String]*): Expect[String] = constructExpect("", whens:_*) - + + def constructExpect[R](defaultValue: R, whens: When[R]*): Expect[R] = + // A little sneaky + val blocks: Seq[ExpectBlock[R]] = if whens.isEmpty then Seq.empty else Seq(ExpectBlock(whens*)) + new Expect("ls", defaultValue)(blocks*) + def testActionsAndResult[R](expect: Expect[R], builder: StringBuilder, expectedResult: R, numberOfAppends: Int = 1): Future[Assertion] = { //Ensure the actions were not executed while constructing and transforming the expect builder.result() shouldBe empty @@ -22,7 +25,7 @@ trait TestUtils extends ScalaFutures with Matchers { test: AsyncTestSuite => obtainedResult shouldBe expectedResult } } - + def testActionsAndFailedResult[R](expect: Expect[R], builder: StringBuilder): Future[Assertion] = { //Ensure the actions were not executed while constructing and transforming the expect builder.result() shouldBe empty diff --git a/src/test/scala/work/martins/simon/expect/BuilderSpec.scala b/src/test/scala/work/martins/simon/expect/ToCoreSpec.scala similarity index 78% rename from src/test/scala/work/martins/simon/expect/BuilderSpec.scala rename to src/test/scala/work/martins/simon/expect/ToCoreSpec.scala index 47f5c5a..c2a5f18 100644 --- a/src/test/scala/work/martins/simon/expect/BuilderSpec.scala +++ b/src/test/scala/work/martins/simon/expect/ToCoreSpec.scala @@ -1,46 +1,35 @@ package work.martins.simon.expect -import org.scalatest.{Matchers, WordSpec} -import work.martins.simon.expect.core.actions._ -import work.martins.simon.expect.dsl.dslToCoreExpect +import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatest.matchers.should.* +import work.martins.simon.expect.core.actions.* +import work.martins.simon.expect.fluent.{given, *} +import work.martins.simon.expect.dsl.given -class BuilderSpec extends WordSpec with Matchers { - def dslSendAndExit(e: dsl.Expect[String]): Unit = { - import e._ +class ToCoreSpec extends AnyWordSpecLike with Matchers: + def dslSendAndExit(e: dsl.Expect[String])(using When[String]): Unit = + import e.* send("string1") exit() - } - def dslAddBlock(e: dsl.Expect[String]): Unit = { - import e._ - e.expect { - addWhens(dslAddWhensEOFAndTimeout) - } - } - def dslAddRegexWhen(e: dsl.Expect[String]): Unit = { - import e._ + def dslAddRegexWhen(e: dsl.Expect[String])(using ExpectBlock[String]): Unit = + import e.* when("""(\d+) \w+""".r) { sendln("string2") } - } - def dslAddWhensEOFAndTimeout(e: dsl.Expect[String]): Unit = { - import e._ + def dslAddWhensEOFAndTimeout(e: dsl.Expect[String])(using ExpectBlock[String]): Unit = + import e.* when(EndOfFile) { exit() } when(Timeout) { exit() } - } - + def fluentSendAndExit(when: fluent.StringWhen[String]): fluent.StringWhen[String] = { when .send("string1") .exit() } - def fluentAddBlock(e: fluent.Expect[String]): fluent.ExpectBlock[String] = { - e.expect - .addWhens(fluentAddWhensEOFAndTimeout) - } def fluentAddRegexWhen(eb: fluent.ExpectBlock[String]): fluent.RegexWhen[String] = { eb.when("""(\d+) \w+""".r) .sendln("string2") @@ -51,10 +40,10 @@ class BuilderSpec extends WordSpec with Matchers { .when(Timeout) .exit() } - - val wrongExpects = Seq( + + val wrongExpects: Seq[dsl.Expect[String]] = Seq( new dsl.Expect(Seq("ls"), defaultValue = "") { - expect { + expectBlock { when("1") { send("string1") exit() @@ -66,7 +55,7 @@ class BuilderSpec extends WordSpec with Matchers { //Missing a expect block }, new dsl.Expect(Seq("ls"), defaultValue = "", Settings.fromConfig()) { - expect { + expectBlock { when("1") { send("string1") exit() @@ -75,18 +64,18 @@ class BuilderSpec extends WordSpec with Matchers { } }, new dsl.Expect("ls", defaultValue = "", Settings()) { - expect{ + expectBlock { when("1"){ send("string1") //Missing an action } } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when("1") { send("string1") exit() @@ -95,34 +84,34 @@ class BuilderSpec extends WordSpec with Matchers { sendln(m => s"string${m.group(1)}") //Different action } } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect{ - when("1"){ + expectBlock { + when("1") { send("string1") returning("result") //Different action } } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect{ + expectBlock { when("1") { send("string1") returningExpect(new dsl.Expect("ls", "")) //Different action } } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when("1") { send("string1") exit() @@ -131,73 +120,73 @@ class BuilderSpec extends WordSpec with Matchers { returning(_ => "result") //Different action } } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when("1") { send("string1") exit() } when("""(\d+) \w+""".r) { - returningExpect(m => new dsl.Expect(Seq("ls"), m.group(1), Settings())) //Different action + returningExpect(m => new dsl.Expect(Seq("ls"), m.group(1))) //Different action } } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when("1") { send("string1") exit() } when("1"){} //StringWhen instead of RegexWhen } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { //RegexWhen instead of StringWhen when("1".r){} when("""(\d+) \w+""".r){} } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when(EndOfFile){} //EndOfFile instead of StringWhen when("""(\d+) \w+""".r){} } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when(Timeout){} //Timeout instead of StringWhen when("""(\d+) \w+""".r){} } - expect{ + expectBlock { when(""){} } }, new dsl.Expect("ls", defaultValue = "") { - expect{ + expectBlock { when("".r){ returning("text") } } } ) - + "An expect" when { "multiple actions without functions are added" should { val coreExpect = new core.Expect("ls", defaultValue = "")( @@ -219,16 +208,16 @@ class BuilderSpec extends WordSpec with Matchers { ) ) ) - + "generate the correct core.Expect from a dsl.Expect" in { val dslExpect = new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when("1") { addActions(dslSendAndExit) } addWhen(dslAddRegexWhen) } - expect { + expectBlock { addWhens(dslAddWhensEOFAndTimeout) } } @@ -236,26 +225,25 @@ class BuilderSpec extends WordSpec with Matchers { } "generate the correct core.Expect from a fluent.Expect" in { val fluentExpect = new fluent.Expect("ls", defaultValue = "") { - expect + expectBlock .when("1") .addActions(fluentSendAndExit) .addWhen(fluentAddRegexWhen) - addExpectBlock(fluentAddBlock) + expectBlock + .addWhens(fluentAddWhensEOFAndTimeout) } fluentExpect.toCore shouldEqual coreExpect } "not generate an equal core.Expect if the expects are different" in { wrongExpects.map(_.toCore == coreExpect) should contain only false - - wrongExpects.map(_.fluentExpect.toCore == coreExpect) should contain only false } } - - //We cannot test that a dsl.Expect generates the correct core.Expect when an Action that contains - //a function (e.g. Returning, SendWithRegex, etc) is used, this happens because - //equality on a function is not defined, which leads to the equals operation on the core.Expect to fail. - //http://stackoverflow.com/questions/20390293/function-equality-notion/20392230#20392230 - //In these cases we just test for structural equality. + + // We cannot test that a dsl.Expect generates the correct core.Expect when an Action that contains + // a function (e.g. Returning, SendWithRegex, etc) is used, this happens because + // equality on a function is not defined, which leads to the equals operation on the core.Expect to fail. + // http://stackoverflow.com/questions/20390293/function-equality-notion/20392230#20392230 + // In these cases we just test for structural equality. "multiple actions with functions are added" should { val coreExpect = new core.Expect("ls", defaultValue = "")( core.ExpectBlock( @@ -279,20 +267,37 @@ class BuilderSpec extends WordSpec with Matchers { ) ) ) - + "generate a structurally equal core.Expect" in { + val fluentExpect = new fluent.Expect("ls", defaultValue = "") { + expectBlock + .when("1") + .addActions(fluentSendAndExit) + .when("""(\d+) (\w+)""".r) + .send(_ => s"Hey this is not the same!") + .returning(_ => "someOtherValue") + .returningExpect(_ => new fluent.Expect("bc", defaultValue = "this is also different")) + expectBlock + .when(EndOfFile) + .returningExpect(new fluent.Expect("bc", defaultValue = "this is also different")) + .when(Timeout) + .returning("someValue") + .exit() + } + fluentExpect.structurallyEquals(coreExpect) shouldBe true + val dslExpect = new dsl.Expect("ls", defaultValue = "") { - expect { + expectBlock { when("1") { addActions(dslSendAndExit) } when("""(\d+) (\w+)""".r) { send(_ => "Hey this is not the same!") returning(_ => "someOtherValue") - returningExpect(_ => new dsl.Expect("bc", defaultValue = "this is also different", Settings.fromConfig())) + returningExpect(_ => new dsl.Expect("bc", defaultValue = "this is also different")) } } - expect { + expectBlock { when(EndOfFile) { returningExpect(new dsl.Expect("bc", defaultValue = "this is also different")) } @@ -303,42 +308,26 @@ class BuilderSpec extends WordSpec with Matchers { } } dslExpect.structurallyEquals(coreExpect) shouldBe true - - val fluentExpect = new fluent.Expect("ls", defaultValue = "") { - expect - .when("1") - .addActions(fluentSendAndExit) - .when("""(\d+) (\w+)""".r) - .send(_ => s"Hey this is not the same!") - .returning(_ => "someOtherValue") - .returningExpect(_ => new fluent.Expect("bc", defaultValue = "this is also different")) - expect - .when(EndOfFile) - .returningExpect(new fluent.Expect("bc", defaultValue = "this is also different")) - .when(Timeout) - .returning("someValue") - .exit() - } - fluentExpect.structurallyEquals(coreExpect) shouldBe true } "not generate a structurally equal core.Expect if the expects are different" in { wrongExpects.map(_.structurallyEquals(coreExpect)) should contain only false - wrongExpects.map(_.fluentExpect.structurallyEquals(coreExpect)) should contain only false - + //Test structurally equals on ActionReturningAction val ara: core.Expect[String] = new dsl.Expect("ls", "") { - expect{ - when("".r){ + expectBlock { + when("".r) { returning("text") } } }.transform( { case "" => new core.Expect("ls", "")() }, { case "text" => "diferentText" } - )//Test structurally equals on ActionReturningActionWithRegex + ) + + //Test structurally equals on ActionReturningActionWithRegex val araWithRegex: core.Expect[String] = new dsl.Expect("ls", "") { - expect{ - when("".r){ + expectBlock { + when("".r) { returning(_ => "text") } } @@ -346,10 +335,9 @@ class BuilderSpec extends WordSpec with Matchers { { case "" => new core.Expect("ls", "")() }, { case "text" => "I see what you did here" } ) - + wrongExpects.map(ara.structurallyEquals(_)) should contain only false wrongExpects.map(araWithRegex.structurallyEquals(_)) should contain only false } } - } -} + } \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/core/CompileTimeErrorsSpec.scala b/src/test/scala/work/martins/simon/expect/core/CompileTimeErrorsSpec.scala index f8fead5..ae3cc79 100644 --- a/src/test/scala/work/martins/simon/expect/core/CompileTimeErrorsSpec.scala +++ b/src/test/scala/work/martins/simon/expect/core/CompileTimeErrorsSpec.scala @@ -1,38 +1,40 @@ package work.martins.simon.expect.core -import org.scalatest.{FlatSpec, Matchers} -import work.martins.simon.expect.core.actions._ +import org.scalatest.flatspec.AnyFlatSpecLike +import org.scalatest.matchers.should.* +import work.martins.simon.expect.core.actions.* +import work.martins.simon.expect.{EndOfFile, Timeout} -class CompileTimeErrorsSpec extends FlatSpec with Matchers { +class CompileTimeErrorsSpec extends AnyFlatSpecLike with Matchers: "A StringWhen" should "not type check if it contains a SendWithRegex" in { - //StringWhen("text")(SendWithRegex(m => m.group(1))) - """StringWhen("text")(SendWithRegex(m => m.group(1)))""" shouldNot typeCheck + //When("text")(Send(m => m.group(1))) + """When("text")(Send(m => m.group(1)))""" shouldNot typeCheck } it should "not type check if it contains a ReturningWithRegex" in { - //StringWhen("text")(ReturningWithRegex(m => m.group(1))) - """StringWhen("text")(ReturningWithRegex(m => m.group(1)))""" shouldNot typeCheck + //When("text")(Returning(m => m.group(1))) + """When("text")(Returning(m => m.group(1)))""" shouldNot typeCheck } it should "not type check if it contains a ReturningExpectWithRegex" in { - //StringWhen("text")(ReturningExpectWithRegex(m => new Expect(m.group(1), "")())) - """StringWhen("text")(ReturningExpectWithRegex(m => new Expect(m.group(1), "")()))""" shouldNot typeCheck + //When("text")(ReturningExpect(m => new Expect(m.group(1), "")())) + """When("text")(ReturningExpect(m => new Expect(m.group(1), "")()))""" shouldNot typeCheck } - + "A RegexWhen" should "compile if it contains a SendWithRegex" in { //This line is a fail fast mechanism - RegexWhen("text".r)(SendWithRegex(m => m.group(1))) - """RegexWhen("text".r)(SendWithRegex(m => m.group(1)))""" should compile + When("text".r)(Send(m => m.group(1))) + """When("text".r)(Send(m => m.group(1)))""" should compile } it should "compile if it contains a ReturningWithRegex" in { //This line is a fail fast mechanism - RegexWhen("text".r)(ReturningWithRegex(m => m.group(1))) - """RegexWhen("text".r)(ReturningWithRegex(m => m.group(1)))""" should compile + When("text".r)(Returning(m => m.group(1))) + """When("text".r)(Returning(m => m.group(1)))""" should compile } it should "compile if it contains a ReturningExpectWithRegex" in { //This line is a fail fast mechanism - RegexWhen("text".r)(ReturningExpectWithRegex(m => new Expect(m.group(1), "")())) - """RegexWhen("text".r)(ReturningExpectWithRegex(m => new Expect(m.group(1), "")()))""" should compile + When("text".r)(ReturningExpect(m => new Expect(m.group(1), "")())) + """When("text".r)(ReturningExpect(m => new Expect(m.group(1), "")()))""" should compile } - + "Actions without regex" should "compile in every When" in { //These lines are a fail fast mechanism val actions: Seq[Action[String, When]] = Seq( @@ -41,10 +43,10 @@ class CompileTimeErrorsSpec extends FlatSpec with Matchers { ReturningExpect(new Expect("ls", "")()), Exit() ) - StringWhen("")(actions:_*) - RegexWhen(".*".r)(actions:_*) - EndOfFileWhen()(actions:_*) - TimeoutWhen()(actions:_*) + When("")(actions*) + When(".*".r)(actions*) + When(EndOfFile)(actions*) + When(Timeout)(actions*) """val actions: Seq[Action[String, When]] = Seq( Send(""), @@ -52,9 +54,8 @@ class CompileTimeErrorsSpec extends FlatSpec with Matchers { ReturningExpect(new Expect("ls", "")()), Exit() ) - StringWhen("")(actions:_*) - RegexWhen(".*".r)(actions:_*) - EndOfFileWhen()(actions:_*) - TimeoutWhen()(actions:_*)""" should compile + When("")(actions*) + When(".*".r)(actions*) + When(EndOfFile)(actions*) + When(Timeout)(actions*)""" should compile } -} diff --git a/src/test/scala/work/martins/simon/expect/core/EmptySpec.scala b/src/test/scala/work/martins/simon/expect/core/EmptySpec.scala index 2714434..4a94160 100644 --- a/src/test/scala/work/martins/simon/expect/core/EmptySpec.scala +++ b/src/test/scala/work/martins/simon/expect/core/EmptySpec.scala @@ -1,34 +1,34 @@ package work.martins.simon.expect.core import java.io.IOException - -import org.scalatest.{AsyncFlatSpec, Matchers} +import org.scalatest.flatspec.AsyncFlatSpec +import org.scalatest.matchers.should.* import work.martins.simon.expect.TestUtils -class EmptySpec extends AsyncFlatSpec with Matchers with TestUtils { +class EmptySpec extends AsyncFlatSpec with Matchers with TestUtils: "An Expect without a command" should "throw IllegalArgumentException" in { assertThrows[IllegalArgumentException] { new Expect("", defaultValue = ())() } } - + "An Expect with a command not available in the system" should "throw IOException" in { assertThrows[IOException] { new Expect("存在しない", defaultValue = ())().run() } } - + val defaultValue = "some nice default value" val baseExpect = new Expect("ls", defaultValue)() def baseExpectWithFunction[T](f: String => T)(t: String): Expect[T] = new Expect("ls", f(t))() def firstWord(value: String): Option[String] = value.split(" ").headOption - + "An Expect without expect blocks" should "return the default value" in { baseExpect.run().map { _ shouldBe defaultValue } } - + it should "map just the default value" in { baseExpect.map(firstWord).run() map { _ shouldBe firstWord(defaultValue) @@ -55,7 +55,7 @@ class EmptySpec extends AsyncFlatSpec with Matchers with TestUtils { _ shouldBe firstWord(defaultValue) } } - + "Transforming when the defaultValue is not in domain" should "throw a NoSuchElementException" in { val thrown = the [NoSuchElementException] thrownBy baseExpect.transform[String]( PartialFunction.empty, @@ -63,7 +63,7 @@ class EmptySpec extends AsyncFlatSpec with Matchers with TestUtils { ) thrown.getMessage should include (defaultValue) } - + "An Expect with an empty expect block" should "fail with IllegalArgumentException" in { assertThrows[IllegalArgumentException] { new Expect("ls", defaultValue = ())( @@ -71,7 +71,7 @@ class EmptySpec extends AsyncFlatSpec with Matchers with TestUtils { ) } } - + "An Expect with an empty when" should "return the default value" in { new Expect("echo ola", defaultValue)( ExpectBlock( @@ -83,4 +83,3 @@ class EmptySpec extends AsyncFlatSpec with Matchers with TestUtils { _ shouldBe defaultValue } } -} diff --git a/src/test/scala/work/martins/simon/expect/core/MapFlatmapTransformSpec.scala b/src/test/scala/work/martins/simon/expect/core/MapFlatmapTransformSpec.scala index 7135d15..8aa1c5b 100644 --- a/src/test/scala/work/martins/simon/expect/core/MapFlatmapTransformSpec.scala +++ b/src/test/scala/work/martins/simon/expect/core/MapFlatmapTransformSpec.scala @@ -1,37 +1,37 @@ package work.martins.simon.expect.core -import org.scalatest.{AsyncWordSpec, BeforeAndAfterEach} -import work.martins.simon.expect.{TestUtils, Timeout} -import work.martins.simon.expect.core.actions._ - +import org.scalatest.BeforeAndAfterEach +import org.scalatest.wordspec.AsyncWordSpec +import work.martins.simon.expect.{EndOfFile, TestUtils, Timeout} +import work.martins.simon.expect.core.actions.* import scala.util.Random import scala.util.matching.Regex.Match -class MapFlatmapTransformSpec extends AsyncWordSpec with BeforeAndAfterEach with TestUtils { +class MapFlatmapTransformSpec extends AsyncWordSpec with BeforeAndAfterEach with TestUtils: val builders = Seq.fill(5)(new StringBuilder("")) val returnedResults = 1 to 5 val defaultValue = returnedResults.sum val expects = Seq( - constructExpect(defaultValue, StringWhen("README")( + constructExpect(defaultValue, When("README")( Returning { appendToBuilder(builders(0)) returnedResults(0) } )), - constructExpect(defaultValue, RegexWhen("LICENSE".r)( - Returning { _: Match => + constructExpect(defaultValue, When("LICENSE".r)( + Returning { (_: Match) => appendToBuilder(builders(1)) returnedResults(1) } )), - constructExpect(defaultValue, RegexWhen("build".r)( - ReturningExpect { _: Match => + constructExpect(defaultValue, When("build".r)( + ReturningExpect { (_: Match) => appendToBuilder(builders(2)) new Expect("ls", returnedResults(2))() } )), - constructExpect(defaultValue, EndOfFileWhen()( + constructExpect(defaultValue, When(EndOfFile)( ReturningExpect { appendToBuilder(builders(3)) new Expect("ls", returnedResults(3))() @@ -62,102 +62,86 @@ class MapFlatmapTransformSpec extends AsyncWordSpec with BeforeAndAfterEach with ) ) ) - - def baseExpect[T](defaultValue: T): Expect[T] = new Expect("ls", defaultValue)() - def baseExpectWithFunction[I, O](f: I => O)(t: I): Expect[O] = new Expect("ls", f(t))() - + override protected def beforeEach(): Unit = builders.foreach(_.clear()) - + builders.zip(returnedResults).zip(expects).foreach { case ((builder, result), expect) => s"The expect ${expect.hashCode()}" when { - def toTuple2(x: Int) = (x, x) + def f(x: Int): String = "NaN" * x + " Batman" + def g(s: String): Double = s.length * Math.PI + "mapped" should { "return the mapped result" in { - testActionsAndResult(expect.map(toTuple2), builder, toTuple2(result)) + testActionsAndResult(expect.map(f), builder, expectedResult = f(result)) } } "flatMapped" should { "return the flatMapped result" in { - testActionsAndResult(expect.flatMap(baseExpectWithFunction(toTuple2)), builder, toTuple2(result)) + val newExpect = expect.flatMap(r => constructExpect(defaultValue = f(r))) + testActionsAndResult(newExpect, builder, expectedResult = f(result)) } } "transformed" should { "throw a NoSuchElementException if the transform function if not defined for some result (in map)" in { val transformedExpect = expect.transform( - // flapMapPF will fail (except for the defaultValue) so transform will have to try mapPF - { case t if t == expect.defaultValue => baseExpect(t)}, - // mapPF will fail too (what we are testing) + // flapMapPF will work just for the defaultValue so transform will have to try mapPF + { case t if t == expect.defaultValue => constructExpect(defaultValue = "a flatMapped value") }, PartialFunction.empty ) testActionsAndFailedResult(transformedExpect, builder) } "throw a NoSuchElementException if the transform function if not defined for some result (in flatMap)" in { val transformedExpect = expect.transform( - // flapMapPF will fail PartialFunction.empty, // Since mapPF is only defined for the defaultValue it will fail for the other values - { case t if t == expect.defaultValue => t } + { case t if t == expect.defaultValue => "a mapped value" } ) testActionsAndFailedResult(transformedExpect, builder) } - - // TODO: remove/change these functions - def mapFunction(x: Int): Seq[Int] = Seq.fill(x)(x) - def flatMapFunction(x: Int): String = "NaN" * x + " Batman" - def flatMap(x: Int): Expect[String] = { - //To make it simple, it just returns the flatMapped defaultValue - new Expect("ls", flatMapFunction(x))() - } - - def mapFunction2(s: String): Int = s.toCharArray.count(_ > 70) - def flatMapFunction2(s: String): Array[Byte] = s.getBytes.filter(_ % 2 == 0) - def flatMap2(s: String): Expect[Array[Byte]] = { - //To make it simple, it just returns the flatMapped defaultValue - new Expect("ls", flatMapFunction2(s))() - } - + val mappedExpect = expect.transform( PartialFunction.empty, - { case t => mapFunction(t).mkString } + { case value => f(value) } ) "return the correct result for: transform (map)" in { - testActionsAndResult(mappedExpect, builder, mapFunction(result).mkString) + testActionsAndResult(mappedExpect, builder, expectedResult = f(result)) } "return the correct result for: transform (map) followed by a map" in { - testActionsAndResult(mappedExpect.map(mapFunction2), builder, mapFunction2(mapFunction(result).mkString)) + testActionsAndResult(mappedExpect.map(g), builder, expectedResult = g(f(result))) } "return the correct result for: transform (map) followed by a flatMap" in { - testActionsAndResult(mappedExpect.flatMap(flatMap2), builder, flatMapFunction2(mapFunction(result).mkString)) + val newExpect = mappedExpect.flatMap(r => constructExpect(defaultValue = g(r))) + testActionsAndResult(newExpect, builder, expectedResult = g(f(result))) } "return the correct result for: transform (map) followed by a transform" in { val transformedExpect = mappedExpect.transform( - { case mappedExpect.defaultValue => flatMap2(mappedExpect.defaultValue) }, - { case _ => Array.ofDim[Byte](5) } + { case mappedExpect.defaultValue => constructExpect(defaultValue = 0) }, + { case _ => 25 } ) - testActionsAndResult(transformedExpect, builder, Array.ofDim[Byte](5)) + testActionsAndResult(transformedExpect, builder, expectedResult = 25) } - - val flatMappedExpect: Expect[String] = expect.transform( - { case t => flatMap(t) }, + + val flatMappedExpect = expect.transform( + { case value => constructExpect(defaultValue = f(value)) }, PartialFunction.empty ) "return the correct result for: transform (flatMap)" in { - testActionsAndResult(flatMappedExpect, builder, flatMapFunction(result)) + testActionsAndResult(flatMappedExpect, builder, expectedResult = f(result)) } "return the correct result for: transform (flatMap) followed by a map" in { - testActionsAndResult(flatMappedExpect.map(mapFunction2), builder, mapFunction2(flatMapFunction(result))) + testActionsAndResult(flatMappedExpect.map(g), builder, expectedResult = g(f(result))) } "return the correct result for: transform (flatMap) followed by a flatMap" in { - testActionsAndResult(flatMappedExpect.flatMap(flatMap2), builder, flatMapFunction2(flatMapFunction(result))) + val newExpect = flatMappedExpect.flatMap(r => constructExpect(defaultValue = g(r))) + testActionsAndResult(newExpect, builder, expectedResult = g(f(result))) } "return the correct result for: transform (flatMap) followed by a transform" in { val transformedExpect = flatMappedExpect.transform( - { case _ => new Expect("ls", Array.ofDim[Byte](5))() }, + { case _ => constructExpect(defaultValue = 25) }, PartialFunction.empty ) - testActionsAndResult(transformedExpect, builder, Array.ofDim[Byte](5)) + testActionsAndResult(transformedExpect, builder, expectedResult = 25) } } } - } -} + } \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/core/ReadFromSpec.scala b/src/test/scala/work/martins/simon/expect/core/ReadFromSpec.scala index 16e08fa..d04def2 100644 --- a/src/test/scala/work/martins/simon/expect/core/ReadFromSpec.scala +++ b/src/test/scala/work/martins/simon/expect/core/ReadFromSpec.scala @@ -1,17 +1,11 @@ package work.martins.simon.expect.core -import org.scalatest.{AsyncFlatSpec, BeforeAndAfterEach} +import org.scalatest.BeforeAndAfterEach +import org.scalatest.flatspec.AsyncFlatSpec import work.martins.simon.expect.TestUtils -/** - * Created by simon on 19-01-2017. - */ -class ReadFromSpec extends AsyncFlatSpec with TestUtils with BeforeAndAfterEach { +class ReadFromSpec extends AsyncFlatSpec with TestUtils with BeforeAndAfterEach: val builder = new StringBuilder("") val expectedValue = "ReturnedValue" - override protected def beforeEach(): Unit = builder.clear() - - - -} + override protected def beforeEach(): Unit = builder.clear() \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/core/ReturningSpec.scala b/src/test/scala/work/martins/simon/expect/core/ReturningSpec.scala index 33f60b0..8119cd1 100644 --- a/src/test/scala/work/martins/simon/expect/core/ReturningSpec.scala +++ b/src/test/scala/work/martins/simon/expect/core/ReturningSpec.scala @@ -1,23 +1,20 @@ package work.martins.simon.expect.core - -import org.scalatest.{AsyncFlatSpec, BeforeAndAfterEach} -import work.martins.simon.expect.TestUtils -import work.martins.simon.expect.core.actions._ - import scala.util.matching.Regex.Match +import org.scalatest.BeforeAndAfterEach +import org.scalatest.flatspec.AsyncFlatSpec +import work.martins.simon.expect.TestUtils +import work.martins.simon.expect.core.actions.* -class ReturningSpec extends AsyncFlatSpec with TestUtils with BeforeAndAfterEach { +class ReturningSpec extends AsyncFlatSpec with TestUtils with BeforeAndAfterEach: val builder = new StringBuilder("") val expectedValue = "ReturnedValue" - + override protected def beforeEach(): Unit = builder.clear() - + "An Expect" should "only return the last returning action before an exit but still execute the previous actions" in { - //should "not execute any action after an exit action" - val expect = constructExpect(When("LICENSE".r)( - //Returning { _: Match => // Why isn't Scala able to infer the correct apply for Returning { _ => ...} - Returning { m: Match => + val expect = constructExpect(defaultValue = "", When("LICENSE".r)( + Returning { m => appendToBuilder(builder) m.group(0) }, @@ -33,7 +30,7 @@ class ReturningSpec extends AsyncFlatSpec with TestUtils with BeforeAndAfterEach )) testActionsAndResult(expect, builder, expectedResult = "b", numberOfAppends = 2) } - + it should "be able to interact with the spawned program" in { val e = new Expect("bc -i", defaultValue = 5)( ExpectBlock( @@ -59,19 +56,13 @@ class ReturningSpec extends AsyncFlatSpec with TestUtils with BeforeAndAfterEach _ shouldBe 6 } } - + it should "fail if an exception is thrown inside an action" in { - // Declaring it Returning[Nothing]{...} makes scala complain about ambiguous reference to overloaded definition - // because Nothing is a subtype of both => R and Match => R. - // Declaring it Returning{...} makes scala complain about dead code following this construct: - // val a = Returning { - // ^ - // Declaring it @silent val a = Returning {...} throws the exception directly, somehow its not captured inside the closure => R. I don't understand why. - val a: Returning[Nothing] = Returning { - appendToBuilder(builder) - throw new NoSuchElementException() - } - val expect = constructExpect("", When("(LICENSE)".r)(a)) + val expect = constructExpect(defaultValue = "", When("(LICENSE)".r) { + Returning[Nothing] { + appendToBuilder(builder) + throw new NoSuchElementException() + } + }) testActionsAndFailedResult(expect, builder) - } -} + } \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/core/StructuralEqualitySpec.scala b/src/test/scala/work/martins/simon/expect/core/StructuralEqualitySpec.scala new file mode 100644 index 0000000..a12eb02 --- /dev/null +++ b/src/test/scala/work/martins/simon/expect/core/StructuralEqualitySpec.scala @@ -0,0 +1,46 @@ +package work.martins.simon.expect.core + +import org.scalatest.flatspec.AnyFlatSpecLike +import org.scalatest.matchers.should.* +import work.martins.simon.expect.core.actions.* +import work.martins.simon.expect.{EndOfFile, Timeout} + +class StructuralEqualitySpec extends AnyFlatSpecLike with Matchers: + val actions = List( + Exit(), + Send("test"), + Sendln(m => s"${m.group(1)}"), + Returning(5), + Returning(m => m.group(1).toInt), + ReturningExpect(new Expect("ls", 6)()), + ReturningExpect(m => new Expect("ls", m.group(1).toInt)()), + ) + actions.foreach { action => + s"${action.getClass.getSimpleName}" should "structurallyEqual itself" in { + action.structurallyEquals(action) shouldBe true + } + it should "not structurallyEqual any other action" in { + actions.filter(_ != action).foreach { other => + action.structurallyEquals(other) shouldBe false + } + } + } + + val whens = List( + When("test")(), + When("test".r)(), + When(Timeout)(), + When(EndOfFile)(), + ) + whens.foreach { when => + s"${when.getClass.getSimpleName}" should "structurallyEqual itself" in { + when.structurallyEquals(when) shouldBe true + } + it should "not structurallyEqual any other whens" in { + whens.filter(_ != when).foreach { other => + when.structurallyEquals(other) shouldBe false + } + } + } + + // ExpectBlocks and Expects are being tested under ToCoreSpec no need to repeat ourselves \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/core/WhenSpec.scala b/src/test/scala/work/martins/simon/expect/core/WhenSpec.scala index 10e5f2c..20e884f 100644 --- a/src/test/scala/work/martins/simon/expect/core/WhenSpec.scala +++ b/src/test/scala/work/martins/simon/expect/core/WhenSpec.scala @@ -2,12 +2,12 @@ package work.martins.simon.expect.core import java.io.EOFException import java.util.concurrent.TimeoutException - -import org.scalatest._ +import org.scalatest.wordspec.AsyncWordSpec +import org.scalatest.matchers.should.* import work.martins.simon.expect.{EndOfFile, TestUtils, Timeout} -import work.martins.simon.expect.core.actions._ +import work.martins.simon.expect.core.actions.* -class WhenSpec extends AsyncWordSpec with Matchers with TestUtils { +class WhenSpec extends AsyncWordSpec with Matchers with TestUtils: "An Expect" when { "the stdOut does not match with any When" should { "run the actions in the TimeoutWhen" in { @@ -59,7 +59,7 @@ class WhenSpec extends AsyncWordSpec with Matchers with TestUtils { } } } - + "eof is read from stdOut" should { "run the actions in the EndOfFileWhen" in { val e = new Expect("ls", defaultValue = "")( @@ -110,7 +110,7 @@ class WhenSpec extends AsyncWordSpec with Matchers with TestUtils { } } } - + "more than one When matches" should { "run the actions in the first matching when" in { val e = new Expect("bc -i", defaultValue = "")( @@ -123,11 +123,10 @@ class WhenSpec extends AsyncWordSpec with Matchers with TestUtils { ) ) ) - + e.run() map { _ should not be "Ohh no" } } } - } -} + } \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/dsl/SyntaxSpec.scala b/src/test/scala/work/martins/simon/expect/dsl/SyntaxSpec.scala index 0dba70b..9cd3da4 100644 --- a/src/test/scala/work/martins/simon/expect/dsl/SyntaxSpec.scala +++ b/src/test/scala/work/martins/simon/expect/dsl/SyntaxSpec.scala @@ -1,117 +1,58 @@ package work.martins.simon.expect.dsl -import org.scalatest.{FlatSpec, Matchers} -import work.martins.simon.expect._ - -class SyntaxSpec extends FlatSpec with Matchers { - def illegalExpectBlocks(e: Expect[String]): Unit = { - import e._ - an [IllegalArgumentException] should be thrownBy expect{} - an [IllegalArgumentException] should be thrownBy expect{ when(""){} } - an [IllegalArgumentException] should be thrownBy expect{ when("", StdErr){} } - an [IllegalArgumentException] should be thrownBy expect{ when("".r){} } - an [IllegalArgumentException] should be thrownBy expect{ when("".r, StdErr){} } - an [IllegalArgumentException] should be thrownBy expect{ when(EndOfFile){} } - an [IllegalArgumentException] should be thrownBy expect{ when(EndOfFile, StdErr){} } - an [IllegalArgumentException] should be thrownBy expect{ when(Timeout){} } - () - } - def illegalWhens(e: Expect[String]): Unit = { - import e._ - an [IllegalArgumentException] should be thrownBy when(""){} - an [IllegalArgumentException] should be thrownBy when("", StdOut){} - an [IllegalArgumentException] should be thrownBy when("".r){} - an [IllegalArgumentException] should be thrownBy when("".r, StdOut){} - an [IllegalArgumentException] should be thrownBy when(EndOfFile){} - an [IllegalArgumentException] should be thrownBy when(EndOfFile, StdOut){} - an [IllegalArgumentException] should be thrownBy when(Timeout){} - () - } - def illegalWhenActions(e: Expect[String]): Unit = { - import e._ - an [IllegalArgumentException] should be thrownBy send("") - an [IllegalArgumentException] should be thrownBy sendln("") - an [IllegalArgumentException] should be thrownBy returning("") - an [IllegalArgumentException] should be thrownBy returningExpect(new Expect("ls", "", Settings.fromConfig())) - an [IllegalArgumentException] should be thrownBy exit() - () - } - def illegalRegexWhenActions(e: Expect[String]): Unit = { - import e._ - an [IllegalArgumentException] should be thrownBy send(_ => "") - an [IllegalArgumentException] should be thrownBy sendln(_ => "") - an [IllegalArgumentException] should be thrownBy returning(_ => "") - an [IllegalArgumentException] should be thrownBy returningExpect(_ => Expect(Seq("ls"), "", Settings())) - () - } +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.* +import work.martins.simon.expect.* +import work.martins.simon.expect.FromInputStream.* +import work.martins.simon.expect.fluent.{Expect as _, *} +class SyntaxSpec extends AnyFlatSpec with Matchers: "An Expect being constructed illegally" should "throw IllegalArgumentException" in { //Without a command an [IllegalArgumentException] should be thrownBy new Expect("", defaultValue = ()) - + new Expect("ls", "") { self => //Empty expect block - an [IllegalArgumentException] should be thrownBy expect {} - - expect { - //Adding expect block inside expect block - addExpectBlock(illegalExpectBlocks) - - //an action directly to the body of an expect block - addActions(illegalWhenActions) - addActions(illegalRegexWhenActions) - - when("") { - //Adding expect block inside when - addExpectBlock(illegalExpectBlocks) - - //Adding when inside when - addWhens(illegalWhens) - - //Adding regex actions inside a string when - addActions(illegalRegexWhenActions) - } + an [IllegalArgumentException] should be thrownBy expectBlock {} + + expectBlock { + //Adding expectBlock inside expectBlock + "expectBlock {}" shouldNot compile + // Prevents error "Expect block cannot be empty." + when(""){} } - - //Adding a when directly to the body of the expect - addWhens(illegalWhens) - - //Adding an action directly to the body of the expect - addActions(illegalWhenActions) - addActions(illegalRegexWhenActions) } } - "An Except being constructed legally" should "not throw IllegalArgumentException" in { - def addExpectBlocks(e: Expect[String]): Unit = { - import e._ - e.expect { + def addExpectBlocks(e: Expect[String]): Unit = + import e.* + expectBlock { addWhens(addMultipleWhens) } - e.expect{ + expectBlock { when("") { addActions(addWhenActions) } } - e.expect{ + expectBlock { when("".r) { addActions(addWhenActions) addActions(addRegexWhenActions) } } - e.expect{ + expectBlock { when(EndOfFile) { addActions(addWhenActions) } } - e.expect{ + expectBlock { when(Timeout) { addActions(addWhenActions) } } - } - def addMultipleWhens(e: Expect[String]): Unit = { - import e._ + + def addMultipleWhens(e: Expect[String])(using ExpectBlock[String]): Unit = + import e.* when("", StdErr){ addActions(addWhenActions) } @@ -125,37 +66,34 @@ class SyntaxSpec extends FlatSpec with Matchers { when(Timeout){ addActions(addWhenActions) } - } - def addWhenActions(e: Expect[String]): Unit = { - import e._ + + def addWhenActions(e: Expect[String])(using When[String]): Unit = + import e.* send("") sendln("") returning("") returningExpect(new Expect("ls", "", Settings.fromConfig())) exit() - } - def addRegexWhenActions(e: Expect[String]): Unit = { - import e._ + + def addRegexWhenActions(e: Expect[String])(using RegexWhen[String]): Unit = + import e.* send(_ => "") sendln(_ => "") returning(_ => "") returningExpect(_ => new Expect(Seq("ls"), "", Settings())) - } - def addEndOfFileWhen(e: Expect[String]): Unit = { - import e._ + + def addEndOfFileWhen(e: Expect[String])(using ExpectBlock[String]): Unit = + import e.* when(EndOfFile) { send("") } - } - + noException should be thrownBy { new Expect("ls", "") { addExpectBlock(addExpectBlocks) - - expect { + expectBlock { addWhen(addEndOfFileWhen) } } } - } -} + } \ No newline at end of file diff --git a/src/test/scala/work/martins/simon/expect/fluent/EmptySpec.scala b/src/test/scala/work/martins/simon/expect/fluent/EmptySpec.scala index ecfe669..a95f3d1 100644 --- a/src/test/scala/work/martins/simon/expect/fluent/EmptySpec.scala +++ b/src/test/scala/work/martins/simon/expect/fluent/EmptySpec.scala @@ -1,29 +1,29 @@ package work.martins.simon.expect.fluent -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.* import work.martins.simon.expect.{Settings, core} -class EmptySpec extends FlatSpec with Matchers { +class EmptySpec extends AnyFlatSpec with Matchers: "An Expect without a command" should "throw IllegalArgumentException" in { an [IllegalArgumentException] should be thrownBy new Expect("", defaultValue = ()) } - + "An Expect with an empty expect block" should "fail when generating the core.Expect" in { val fe = new Expect(Seq("ls"), defaultValue = (), Settings()) { - expect + expectBlock } an [IllegalArgumentException] should be thrownBy fe.toCore } - - "Invoking expect.expect" should "fail when generating the core.Expect" in { + + "Invoking expectBlock.expectBlock" should "fail when generating the core.Expect" in { val fe = new Expect(Seq("ls"), defaultValue = (), Settings.fromConfig()) { - expect.expect + expectBlock.expectBlock } an [IllegalArgumentException] should be thrownBy fe.toCore } - + "An Expect without expect blocks" should "generate the correct core.Expect" in { val fe = new Expect("ls", defaultValue = (), Settings()) fe.toCore shouldEqual new core.Expect("ls", defaultValue = ())() - } -} + } \ No newline at end of file diff --git a/version.sbt b/version.sbt index f5502ff..6a0fb29 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "7.0.1-SNAPSHOT" +ThisBuild / version := "7.0.1-SNAPSHOT"