From 8d008d9a7956e2f2f652dfe4dfac2b1115473d0c Mon Sep 17 00:00:00 2001 From: etorreborre Date: Sun, 21 Nov 2021 15:50:48 +0100 Subject: [PATCH 01/11] backported the fix for #1024 for the ScalaCheck seed --- .../org/specs2/scalacheck/Parameters.scala | 3 +- .../scalacheck/ScalaCheckPropertyCheck.scala | 46 ++++++++++--------- .../ScalaCheckMatchersResultsSpec.scala | 21 +++++++++ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/scalacheck/shared/src/main/scala/org/specs2/scalacheck/Parameters.scala b/scalacheck/shared/src/main/scala/org/specs2/scalacheck/Parameters.scala index 5463ca8ef3..f7044c511a 100644 --- a/scalacheck/shared/src/main/scala/org/specs2/scalacheck/Parameters.scala +++ b/scalacheck/shared/src/main/scala/org/specs2/scalacheck/Parameters.scala @@ -35,7 +35,8 @@ case class Parameters(minTestsOk: Int = Test.Parameters.default. withMinSize(outer.minSize). withWorkers(outer.workers). withTestCallback(outer.testCallback). - withCustomClassLoader(outer.loader) + withCustomClassLoader(outer.loader). + withInitialSeed(outer.seed) } def overrideWith(commandLine: CommandLine): Parameters = { diff --git a/scalacheck/shared/src/main/scala/org/specs2/scalacheck/ScalaCheckPropertyCheck.scala b/scalacheck/shared/src/main/scala/org/specs2/scalacheck/ScalaCheckPropertyCheck.scala index 34c0ccc186..a997fa46ce 100644 --- a/scalacheck/shared/src/main/scala/org/specs2/scalacheck/ScalaCheckPropertyCheck.scala +++ b/scalacheck/shared/src/main/scala/org/specs2/scalacheck/ScalaCheckPropertyCheck.scala @@ -25,33 +25,35 @@ trait ScalaCheckPropertyCheck extends ExpectationsCreation { * parameters */ def check(prop: Prop, parameters: Parameters, prettyFreqMap: FreqMap[Set[Any]] => Pretty): Result = { - // this should not happen but the impossible is known to happen + /** SEED CAPTURE We capture the first seed used to run the property in order to be able to display it. This seed is + * either the first random seed used by ScalaCheck, which we capture with a mutable variable, or the seed set by + * the user + */ var capturedSeed: Seed = null - lazy val initialSeed = Option(capturedSeed).orElse(parameters.seed).getOrElse( - throw new Exception("A seed could not be captured for a ScalaCheck property and no seed was set on the prop or " + - "set on the command line. Please report this issue to http://github.com/etorreborre/specs2/issues")) - - val prop1 = parameters.seed match { - case None => - Prop { prms0 => - val (prms, seed) = prms0.initialSeed match { - case Some(sd) => - (prms0, sd) - case None => - val sd = Seed.random() - (prms0.withInitialSeed(sd), sd) - } - val res = prop(prms) - capturedSeed = seed - res + lazy val initialSeed = Option(capturedSeed) + .orElse(parameters.seed) + .getOrElse( + throw new Exception( + "A seed could not be captured for a ScalaCheck property and no seed was set on the prop or " + + "set on the command line. Please report this issue to http://github.com/etorreborre/specs2/issues" + ) + ) + + // make a new property which will capture the first seed used by the property + def propWithCapturedSeed = Prop { prms0 => + val (prms, seed) = prms0.initialSeed match { + case Some(sd) => (prms0, sd) + case _ => { + val sd = Seed.random() + (prms0.withInitialSeed(sd), sd) } - - case Some(s) => - prop.useSeed("specs2", s) + } + capturedSeed = seed + prop(prms) } - val result = Test.check(parameters.testParameters, prop1) + val result = Test.check(parameters.testParameters, propWithCapturedSeed) val prettyTestResult = prettyResult(result, parameters, initialSeed, prettyFreqMap)(parameters.prettyParams) val testResult = if (parameters.prettyParams.verbosity == 0) "" else prettyTestResult diff --git a/tests/src/test/scala/org/specs2/scalacheck/ScalaCheckMatchersResultsSpec.scala b/tests/src/test/scala/org/specs2/scalacheck/ScalaCheckMatchersResultsSpec.scala index 06faafa4e3..ff25b37c90 100644 --- a/tests/src/test/scala/org/specs2/scalacheck/ScalaCheckMatchersResultsSpec.scala +++ b/tests/src/test/scala/org/specs2/scalacheck/ScalaCheckMatchersResultsSpec.scala @@ -159,3 +159,24 @@ class TSpec extends mutable.Specification with ScalaCheck { true } } + +class SeedSpec extends Specification with ScalaCheck { + def is = sequential ^ s2""" + A seed can be set on a property $runProperty + The generated values must be different $checkValues + """ + + var generated: List[(Int, Int)] = List() + + def runProperty = prop { (x: Int, y: Int) => + generated = generated :+ ((x, y)) + ok + }.setSeed("5dHu0rwf1jZ22C-BHl3poKhOY8iXY19a9jdB0JL6ZIJ=") + + def checkValues = { + // we expected at least 50 different generated values + (generated.distinct.size must be_>=(50)) and + // the first result depends on the initial seed + (generated.head must (===((1, 2147483647)))) + } +} From 63004d937f5e3c3f5c893fa3b89304bf9c24b6c9 Mon Sep 17 00:00:00 2001 From: etorreborre Date: Sun, 21 Nov 2021 16:08:02 +0100 Subject: [PATCH 02/11] fix: backported the fix for #1020 --- .../scala/org/specs2/reporter/SbtPrinter.scala | 11 ++++++++++- .../scala/org/specs2/runner/SbtRunner.scala | 18 ++++++------------ .../org/specs2/reporter/SbtPrinterSpec.scala | 3 ++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/core/shared/src/main/scala/org/specs2/reporter/SbtPrinter.scala b/core/shared/src/main/scala/org/specs2/reporter/SbtPrinter.scala index 442c54c236..df6ee052a4 100644 --- a/core/shared/src/main/scala/org/specs2/reporter/SbtPrinter.scala +++ b/core/shared/src/main/scala/org/specs2/reporter/SbtPrinter.scala @@ -25,6 +25,9 @@ trait SbtPrinter extends Printer { /** events handler to notify Sbt of successes/failures */ def events: SbtEvents + /** specifies if only sbt events must be emitted */ + def eventsOnly: Boolean + lazy val textPrinter = TextPrinter def sbtNotifierPrinter(args: Arguments): Printer = @@ -36,7 +39,7 @@ trait SbtPrinter extends Printer { * - one for registering sbt events */ def sink(env: Env, spec: SpecStructure): AsyncSink[Fragment] = - textSink(env, spec) <* eventSink(env, spec) + if (eventsOnly) eventSink(env, spec) else textSink(env, spec) <* eventSink(env, spec) def textSink(env: Env, spec: SpecStructure): AsyncSink[Fragment] = textPrinter.sink(env.setLineLogger(SbtLineLogger(loggers)), spec) @@ -47,6 +50,12 @@ trait SbtPrinter extends Printer { object SbtPrinter { + def makeSbtPrinter(_loggers: Array[Logger], _events: SbtEvents, _eventsOnly: Boolean): SbtPrinter = new SbtPrinter { + lazy val loggers = _loggers + lazy val events = _events + lazy val eventsOnly = _eventsOnly + } + @annotation.nowarn def sbtNotifier(events: SbtEvents, args: Arguments) = new Notifier { private val context: scala.collection.mutable.Stack[String] = diff --git a/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala b/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala index a03eef7a10..02881aa730 100644 --- a/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala +++ b/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala @@ -200,24 +200,18 @@ case class SbtTask(aTaskDef: TaskDef, env: Env, loader: ClassLoader) extends sbt /** accepted printers */ private def createPrinters(taskDef: TaskDef, handler: EventHandler, loggers: Array[Logger], args: Arguments): Operation[List[Printer]] = List( - createSbtPrinter(handler, loggers, sbtEvents(taskDef, handler)), + createSbtPrinter(loggers, sbtEvents(taskDef, handler)), createJUnitXmlPrinter(args, loader), createHtmlPrinter(args, loader), createMarkdownPrinter(args, loader), createPrinter(args, loader), createNotifierPrinter(args, loader)).map(_.map(_.toList)).sequence.map(_.flatten) - private def createSbtPrinter(h: EventHandler, ls: Array[Logger], e: SbtEvents) = { - if (!printerNames.map(_.name).exists(arguments.isSet) || arguments.isSet(CONSOLE.name)) - Operations.ok(Some { - new SbtPrinter { - lazy val handler = h - lazy val loggers = ls - lazy val events = e - } - }) - else noInstance("no console printer defined", arguments.verbose) - } + private def createSbtPrinter(ls: Array[Logger], e: SbtEvents) = + Operations.ok(Some { + SbtPrinter.makeSbtPrinter(ls, e, + _eventsOnly = printerNames.map(_.name).exists(arguments.isSet) && !arguments.isSet(CONSOLE.name)) + }) private def sbtEvents(t: TaskDef, h: EventHandler) = new SbtEvents { lazy val taskDef = t diff --git a/tests/src/test/scala/org/specs2/reporter/SbtPrinterSpec.scala b/tests/src/test/scala/org/specs2/reporter/SbtPrinterSpec.scala index 471ab35c19..2f13119d88 100644 --- a/tests/src/test/scala/org/specs2/reporter/SbtPrinterSpec.scala +++ b/tests/src/test/scala/org/specs2/reporter/SbtPrinterSpec.scala @@ -76,6 +76,7 @@ class SbtPrinterSpec(val env: Env) extends Spec with OwnEnv { def is = s2""" lazy val handler = outer.handler lazy val taskDef = new TaskDef("", Fingerprints.fp1, true, Array()) } + lazy val eventsOnly = false } } @@ -83,13 +84,13 @@ class SbtPrinterSpec(val env: Env) extends Spec with OwnEnv { def is = s2""" case class printer2() extends Mockito { outer => val logger = mock[Logger] val handler = mock[EventHandler] - val printer = new SbtPrinter { lazy val loggers = Array(logger) lazy val events = new SbtEvents { lazy val handler = outer.handler lazy val taskDef = new TaskDef("", Fingerprints.fp1, true, Array()) } + val eventsOnly = false } def e1 = { From e2775392cd05c2ee32c471a222ed0d57c80cb8ad Mon Sep 17 00:00:00 2001 From: etorreborre Date: Thu, 25 Nov 2021 15:45:46 +0100 Subject: [PATCH 03/11] fix: #1027 fixed the passing on number of threads to use from a specification --- .../src/main/scala/org/specs2/specification/core/Env.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/org/specs2/specification/core/Env.scala b/core/shared/src/main/scala/org/specs2/specification/core/Env.scala index e6a2fd4f94..9dcf898ca4 100644 --- a/core/shared/src/main/scala/org/specs2/specification/core/Env.scala +++ b/core/shared/src/main/scala/org/specs2/specification/core/Env.scala @@ -80,7 +80,9 @@ case class Env( /** set new arguments */ def setArguments(args: Arguments) = - copy(arguments = args) + copy(arguments = args, + executionEnv = ExecutionEnv.create(args, systemLogger), + specs2ExecutionEnv = ExecutionEnv.createSpecs2(args, systemLogger)) /** @return an isolated env */ def setWithIsolation = From a7666f37ee4dbbd6455c7c0c55e6baa411174f8b Mon Sep 17 00:00:00 2001 From: etorreborre Date: Thu, 25 Nov 2021 16:11:44 +0100 Subject: [PATCH 04/11] project: publish 4.13.1 --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 48ec28b2bb..b8b575f039 100644 --- a/version.sbt +++ b/version.sbt @@ -1,2 +1,2 @@ -ThisBuild / version := "4.13.0" +ThisBuild / version := "4.13.1" ThisBuild / versionScheme := Some("semver-spec") From 2772dacc84723c4635e193d0552d3dabe9716383 Mon Sep 17 00:00:00 2001 From: etorreborre Date: Thu, 13 Jan 2022 09:41:34 +0100 Subject: [PATCH 05/11] fix: #1048 fixed the threads leak following #1027 --- .../specification/core/EnvDefault.scala | 15 ++++++++++----- .../specification/core/EnvDefault.scala | 19 +++++++++++++------ .../scala/org/specs2/runner/ClassRunner.scala | 2 +- .../org/specs2/specification/core/Env.scala | 4 +--- version.sbt | 2 +- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/core/js-native/src/main/scala/org/specs2/specification/core/EnvDefault.scala b/core/js-native/src/main/scala/org/specs2/specification/core/EnvDefault.scala index 1e2da2cd2c..64e784cd2c 100644 --- a/core/js-native/src/main/scala/org/specs2/specification/core/EnvDefault.scala +++ b/core/js-native/src/main/scala/org/specs2/specification/core/EnvDefault.scala @@ -5,26 +5,31 @@ package core import control._ import io.FileSystem import main.Arguments -import reporter.LineLogger.NoLineLogger +import reporter._, LineLogger._ import specification.process._ +import concurrent._ import reflect._ object EnvDefault { def default: Env = + create(Arguments(), consoleLogger) + + def create(arguments: Arguments, lineLogger: LineLogger): Env = Env( - arguments = Arguments(), + arguments = arguments, systemLogger = consoleLogging, selectorInstance = (arguments: Arguments) => DefaultSelector, executorInstance = (arguments: Arguments) => DefaultExecutor, - lineLogger = NoLineLogger, + lineLogger = lineLogger, statsRepository = (arguments: Arguments) => StatisticsRepositoryCreation.memory, random = new scala.util.Random, fileSystem = new FileSystem {}, executionParameters = ExecutionParameters(), customClassLoader = None, - classLoading = new ClassLoading {} - ) + classLoading = new ClassLoading {}, + executionEnv = ExecutionEnv.create(arguments, consoleLogging), + specs2ExecutionEnv = ExecutionEnv.createSpecs2(arguments, consoleLogging)) def defaultInstances(env: Env) = List[AnyRef]( diff --git a/core/jvm/src/main/scala/org/specs2/specification/core/EnvDefault.scala b/core/jvm/src/main/scala/org/specs2/specification/core/EnvDefault.scala index a7352ec5fd..807af4033f 100644 --- a/core/jvm/src/main/scala/org/specs2/specification/core/EnvDefault.scala +++ b/core/jvm/src/main/scala/org/specs2/specification/core/EnvDefault.scala @@ -2,29 +2,36 @@ package org.specs2 package specification package core -import org.specs2.control.consoleLogging +import org.specs2.control._ import org.specs2.io.FileSystem import org.specs2.io._ +import org.specs2.concurrent.ExecutionEnv import org.specs2.main.Arguments import org.specs2.reflect.ClassLoading -import org.specs2.reporter.LineLogger.NoLineLogger +import org.specs2.reporter._, LineLogger._ import org.specs2.specification.process._ object EnvDefault { def default: Env = + create(Arguments(), consoleLogger) + + def create(arguments: Arguments, lineLogger: LineLogger): Env = Env( - arguments = Arguments(), - systemLogger = consoleLogging, + arguments = arguments, + systemLogger = consoleLogging, selectorInstance = (arguments: Arguments) => Arguments.instance(arguments.select.selector).getOrElse(DefaultSelector), executorInstance = (arguments: Arguments) => Arguments.instance(arguments.execute.executor).getOrElse(DefaultExecutor), - lineLogger = NoLineLogger, + lineLogger = lineLogger, statsRepository = (arguments: Arguments) => StatisticsRepositoryCreation.file(arguments.commandLine.directoryOr("stats.outdir", "target" / "specs2-reports" / "stats")), random = new scala.util.Random, fileSystem = FileSystem, executionParameters = ExecutionParameters(), customClassLoader = None, - classLoading = new ClassLoading {}) + classLoading = new ClassLoading {}, + executionEnv = ExecutionEnv.create(arguments, consoleLogging), + specs2ExecutionEnv = ExecutionEnv.createSpecs2(arguments, consoleLogging) + ) def defaultInstances(env: Env) = List[AnyRef](env.arguments.commandLine, diff --git a/core/shared/src/main/scala/org/specs2/runner/ClassRunner.scala b/core/shared/src/main/scala/org/specs2/runner/ClassRunner.scala index b4c50631f4..f8858cef68 100644 --- a/core/shared/src/main/scala/org/specs2/runner/ClassRunner.scala +++ b/core/shared/src/main/scala/org/specs2/runner/ClassRunner.scala @@ -32,7 +32,7 @@ trait ClassRunner { val arguments = Arguments(args.drop(1): _*) arguments.reportUnknown() - val env = Env(arguments = arguments, lineLogger = consoleLogger) + val env = EnvDefault.create(arguments, consoleLogger) val actions: Action[Stats] = args.toList match { case Nil => diff --git a/core/shared/src/main/scala/org/specs2/specification/core/Env.scala b/core/shared/src/main/scala/org/specs2/specification/core/Env.scala index 9dcf898ca4..e6a2fd4f94 100644 --- a/core/shared/src/main/scala/org/specs2/specification/core/Env.scala +++ b/core/shared/src/main/scala/org/specs2/specification/core/Env.scala @@ -80,9 +80,7 @@ case class Env( /** set new arguments */ def setArguments(args: Arguments) = - copy(arguments = args, - executionEnv = ExecutionEnv.create(args, systemLogger), - specs2ExecutionEnv = ExecutionEnv.createSpecs2(args, systemLogger)) + copy(arguments = args) /** @return an isolated env */ def setWithIsolation = diff --git a/version.sbt b/version.sbt index b8b575f039..81098b005c 100644 --- a/version.sbt +++ b/version.sbt @@ -1,2 +1,2 @@ -ThisBuild / version := "4.13.1" +ThisBuild / version := "4.13.2" ThisBuild / versionScheme := Some("semver-spec") From 16b07771f0d66c9825be1dd25242c883df7d1542 Mon Sep 17 00:00:00 2001 From: etorreborre Date: Mon, 14 Feb 2022 09:23:41 +0100 Subject: [PATCH 06/11] fix: skip a timeout exception only when setting a timeout argument any timeout exception thrown by an example will be considered as an error --- .../main/scala/org/specs2/execute/ResultExecution.scala | 2 -- .../scala/org/specs2/specification/core/Execution.scala | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/common/shared/src/main/scala/org/specs2/execute/ResultExecution.scala b/common/shared/src/main/scala/org/specs2/execute/ResultExecution.scala index 7ccea9d630..e652feae35 100644 --- a/common/shared/src/main/scala/org/specs2/execute/ResultExecution.scala +++ b/common/shared/src/main/scala/org/specs2/execute/ResultExecution.scala @@ -7,7 +7,6 @@ import reflect.ClassName._ import text.NotNullStrings._ import java.util.regex.Pattern import scala.util.control.NonFatal -import scala.concurrent.TimeoutException /** * This trait executes a Result and returns an appropriate value when a specs2 exception is thrown @@ -35,7 +34,6 @@ trait ResultExecution { outer => case e: AssertionError => Error(e) case e: java.lang.Error if simpleClassName(e) == "NotImplementedError" => Failure(e.getMessage.notNull, "", e.getStackTrace.toList, details = FromJUnitAssertionError) case e: java.lang.Error if simpleClassName(e) == "ExpectationError" => Failure(e.toString, "", e.getStackTrace.toList, details = FromExpectationError) - case e: TimeoutException => Skipped(e.getMessage) case NonFatal(t) => Error(t) } diff --git a/core/shared/src/main/scala/org/specs2/specification/core/Execution.scala b/core/shared/src/main/scala/org/specs2/specification/core/Execution.scala index a056ca7362..c7be6a3181 100644 --- a/core/shared/src/main/scala/org/specs2/specification/core/Execution.scala +++ b/core/shared/src/main/scala/org/specs2/specification/core/Execution.scala @@ -146,15 +146,20 @@ case class Execution(run: Option[Env => Future[() => Result]] = None, }, to) val future = timedFuture.runNow(env.executorServices).recoverWith { - // // this exception is thrown if the `action()` code above throws an exception + // this exception is thrown if the `action()` code above throws an exception case e: ExecutionException => if (NonFatal(e.getCause)) Future.successful((ResultExecution.handleExceptionsPurely(e.getCause), timer.stop)) else Future.failed(FatalExecution(e.getCause)) + // we catch timeout exceptions here when they are caused by the timeout argument + // and the skip the corresponding example + case e: TimeoutException => + Future.successful((Skipped(e.getMessage), timer.stop)) + case NonFatal(e) => - // Future execution could still throw FailureExceptions or TimeoutExceptions + // Future execution could still throw FailureExceptions // which can only be recovered here Future.successful((ResultExecution.handleExceptionsPurely(e), timer.stop)) } From 0bcec6df9d4938816fe5968eac56e544d5da21ac Mon Sep 17 00:00:00 2001 From: etorreborre Date: Mon, 14 Feb 2022 09:26:02 +0100 Subject: [PATCH 07/11] project: release 4.13.3 --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 81098b005c..ea84465234 100644 --- a/version.sbt +++ b/version.sbt @@ -1,2 +1,2 @@ -ThisBuild / version := "4.13.2" +ThisBuild / version := "4.13.3" ThisBuild / versionScheme := Some("semver-spec") From 4ffc96e9cd45210b5967dd5f2521c386d45ce0b2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 16 Feb 2022 00:13:40 -0800 Subject: [PATCH 08/11] Re-enable native cross-build (#1065) * Re-enable native cross-build * Regenerate workflow * Use parasitic EC for native * No getSuperclass on native * Disable thin client * Use scalanative.runtime.loop(), dump parasitic ec * Fix compile --- .github/workflows/ci.yml | 10 +- build.sbt | 16 +- .../src/main/scala/org/specs2/Platform.scala | 7 + .../org/specs2/concurrent/ExecutionEnv.scala | 0 .../specs2/concurrent/ExecutorServices.scala | 0 .../scala/org/specs2/concurrent/package.scala | 12 ++ .../src/main/scala/org/specs2/Platform.scala | 7 + .../scala/org/specs2/concurrent/package.scala | 12 ++ .../src/main/scala/org/specs2/Platform.scala | 7 + .../org/specs2/concurrent/ExecutionEnv.scala | 49 +++++ .../specs2/concurrent/ExecutorServices.scala | 48 +++++ .../scala/org/specs2/concurrent/package.scala | 13 ++ .../scala/concurrent/ExecutionContext.scala | 182 ------------------ .../scala/org/specs2/reflect/ClassName.scala | 4 +- .../org/specs2/control/ExecuteActions.scala | 0 .../org/specs2/control/ExecuteActions.scala | 80 ++++++++ .../scala/org/specs2/runner/SbtRunner.scala | 3 +- .../scala/org/specs2/matcher/Matchers.scala | 51 +++++ project/build.properties | 2 +- project/depends.scala | 10 +- project/plugins.sbt | 6 +- 21 files changed, 314 insertions(+), 205 deletions(-) create mode 100644 common/js/src/main/scala/org/specs2/Platform.scala rename common/{js-native => js}/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala (100%) rename common/{js-native => js}/src/main/scala/org/specs2/concurrent/ExecutorServices.scala (100%) create mode 100644 common/js/src/main/scala/org/specs2/concurrent/package.scala create mode 100644 common/jvm/src/main/scala/org/specs2/Platform.scala create mode 100644 common/jvm/src/main/scala/org/specs2/concurrent/package.scala create mode 100644 common/native/src/main/scala/org/specs2/Platform.scala create mode 100644 common/native/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala create mode 100644 common/native/src/main/scala/org/specs2/concurrent/ExecutorServices.scala create mode 100644 common/native/src/main/scala/org/specs2/concurrent/package.scala delete mode 100644 common/native/src/main/scala/scala/concurrent/ExecutionContext.scala rename core/{js-native => js}/src/main/scala/org/specs2/control/ExecuteActions.scala (100%) create mode 100644 core/native/src/main/scala/org/specs2/control/ExecuteActions.scala create mode 100644 matcher/native/src/main/scala/org/specs2/matcher/Matchers.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1614fe2e24..dd33dac76e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.14] + scala: [2.12.15] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -49,10 +49,10 @@ jobs: key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - name: Check that workflows are up to date - run: sbt --client '++${{ matrix.scala }}; githubWorkflowCheck' + run: sbt ++${{ matrix.scala }} githubWorkflowCheck - name: Build project - run: sbt --client '++${{ matrix.scala }}; testOnly -- xonly exclude ci' + run: sbt ++${{ matrix.scala }} 'testOnly -- xonly exclude ci' publish: name: Publish Artifacts @@ -61,7 +61,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.14] + scala: [2.12.15] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -88,4 +88,4 @@ jobs: key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - name: Publish project - run: sbt --client '++${{ matrix.scala }}; +publish' \ No newline at end of file + run: sbt ++${{ matrix.scala }} +publish \ No newline at end of file diff --git a/build.sbt b/build.sbt index 296cf45345..65dbd7cf61 100644 --- a/build.sbt +++ b/build.sbt @@ -15,6 +15,7 @@ lazy val specs2 = project.in(file(".")). name := "specs2", packagedArtifacts := Map.empty, ThisBuild / githubWorkflowArtifactUpload := false, + ThisBuild / githubWorkflowUseSbtThinClient := false, ThisBuild / githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("testOnly -- xonly exclude ci"), name = Some("Build project"))), Global / onChangedBuildSource := ReloadOnSourceChanges, test := {} @@ -22,7 +23,9 @@ lazy val specs2 = project.in(file(".")). fpJVM, catsJVM, commonJVM, matcherJVM, coreJVM, matcherExtraJVM, scalazJVM, html, analysisJVM, shapelessJVM, formJVM, markdownJVM, gwtJVM, junitJVM, scalacheckJVM, mockJVM, xmlJVM, tests, fpJS, catsJS, commonJS, matcherJS, coreJS, matcherExtraJS, scalazJS, analysisJS, - shapelessJS, junitJS, scalacheckJS, mockJS + shapelessJS, junitJS, scalacheckJS, mockJS, fpNative, catsNative, commonNative, matcherNative, + coreNative, matcherExtraNative, scalazNative, analysisNative, shapelessNative, junitNative, + scalacheckNative, mockNative ) /** COMMON SETTINGS */ @@ -30,9 +33,9 @@ lazy val specs2Settings = Seq( organization := "org.specs2", GlobalScope / scalazVersion := "7.2.32", specs2ShellPrompt, - scalaVersion := "2.13.6", + scalaVersion := "2.13.8", SettingKey[Boolean]("ide-skip-project").withRank(KeyRanks.Invisible) := platformDepsCrossVersion.value == ScalaNativeCrossVersion.binary, - crossScalaVersions := Seq(scalaVersion.value, "2.12.14")) + crossScalaVersions := Seq(scalaVersion.value, "2.12.15")) lazy val tagName = Def.setting { s"specs2-${version.value}" @@ -52,8 +55,6 @@ lazy val commonJsSettings = Seq( ) ++ depends.jsMacrotaskExecutor lazy val commonNativeSettings = Seq( - scalaVersion := "2.13", - crossScalaVersions := Seq("2.13"), nativeLinkStubs := true ) @@ -96,7 +97,7 @@ lazy val analysisJVM = analysis.jvm lazy val analysisJS = analysis.js lazy val analysisNative = analysis.native -lazy val cats = crossProject(JSPlatform, JVMPlatform).in(file("cats")). +lazy val cats = crossProject(JSPlatform, JVMPlatform, NativePlatform).in(file("cats")). settings( commonSettings, libraryDependencies ++= Seq( @@ -111,6 +112,7 @@ lazy val cats = crossProject(JSPlatform, JVMPlatform).in(file("cats")). lazy val catsJS = cats.js lazy val catsJVM = cats.jvm +lazy val catsNative = cats.native lazy val common = crossProject(JSPlatform, JVMPlatform, NativePlatform).in(file("common")). settings( @@ -445,7 +447,7 @@ lazy val compilationSettings = Seq( "-Xlint:-byname-implicit") } }, - addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.13.0" cross CrossVersion.full), + addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full), Test / scalacOptions += "-Yrangepos", Compile / doc / scalacOptions ++= Seq("-feature", "-language:_"), Compile / console / scalacOptions := Seq("-Yrangepos", "-feature", "-language:_"), diff --git a/common/js/src/main/scala/org/specs2/Platform.scala b/common/js/src/main/scala/org/specs2/Platform.scala new file mode 100644 index 0000000000..7186daa203 --- /dev/null +++ b/common/js/src/main/scala/org/specs2/Platform.scala @@ -0,0 +1,7 @@ +package org.specs2 + +private[specs2] object Platform { + final val isJVM = false + final val isJS = true + final val isNative = false +} diff --git a/common/js-native/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala b/common/js/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala similarity index 100% rename from common/js-native/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala rename to common/js/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala diff --git a/common/js-native/src/main/scala/org/specs2/concurrent/ExecutorServices.scala b/common/js/src/main/scala/org/specs2/concurrent/ExecutorServices.scala similarity index 100% rename from common/js-native/src/main/scala/org/specs2/concurrent/ExecutorServices.scala rename to common/js/src/main/scala/org/specs2/concurrent/ExecutorServices.scala diff --git a/common/js/src/main/scala/org/specs2/concurrent/package.scala b/common/js/src/main/scala/org/specs2/concurrent/package.scala new file mode 100644 index 0000000000..1da742bf5e --- /dev/null +++ b/common/js/src/main/scala/org/specs2/concurrent/package.scala @@ -0,0 +1,12 @@ +package org.specs2 + +import scala.concurrent.{ Await, Awaitable } +import scala.concurrent.duration.Duration + +package object concurrent { + + private[specs2] def awaitResult[A](a: Awaitable[A], d: Duration) = { + Await.result(a, d) + } + +} diff --git a/common/jvm/src/main/scala/org/specs2/Platform.scala b/common/jvm/src/main/scala/org/specs2/Platform.scala new file mode 100644 index 0000000000..52cbaefc4a --- /dev/null +++ b/common/jvm/src/main/scala/org/specs2/Platform.scala @@ -0,0 +1,7 @@ +package org.specs2 + +private[specs2] object Platform { + final val isJVM = true + final val isJS = false + final val isNative = false +} diff --git a/common/jvm/src/main/scala/org/specs2/concurrent/package.scala b/common/jvm/src/main/scala/org/specs2/concurrent/package.scala new file mode 100644 index 0000000000..1da742bf5e --- /dev/null +++ b/common/jvm/src/main/scala/org/specs2/concurrent/package.scala @@ -0,0 +1,12 @@ +package org.specs2 + +import scala.concurrent.{ Await, Awaitable } +import scala.concurrent.duration.Duration + +package object concurrent { + + private[specs2] def awaitResult[A](a: Awaitable[A], d: Duration) = { + Await.result(a, d) + } + +} diff --git a/common/native/src/main/scala/org/specs2/Platform.scala b/common/native/src/main/scala/org/specs2/Platform.scala new file mode 100644 index 0000000000..aecfbc22aa --- /dev/null +++ b/common/native/src/main/scala/org/specs2/Platform.scala @@ -0,0 +1,7 @@ +package org.specs2 + +private[specs2] object Platform { + final val isJVM = false + final val isJS = false + final val isNative = true +} diff --git a/common/native/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala b/common/native/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala new file mode 100644 index 0000000000..a497c8c0cf --- /dev/null +++ b/common/native/src/main/scala/org/specs2/concurrent/ExecutionEnv.scala @@ -0,0 +1,49 @@ +package org.specs2 +package concurrent + +import org.specs2.control.Logger +import org.specs2.main.Arguments + +import scala.concurrent.ExecutionContext + +/** + * Execution environment for native + */ +case class ExecutionEnv(executorServices: ExecutorServices, + timeFactor: Int, + retriesFactor: Int) { + + def shutdown(): Unit = () + + def setTimeFactor(tf: Int): ExecutionEnv = + copy(timeFactor = tf) + + def setRetriesFactor(tf: Int): ExecutionEnv = + copy(retriesFactor = tf) + + lazy val executionContext = executorServices.executionContext + lazy val scheduler = executorServices.scheduler + + implicit lazy val ec = executorServices.executionContext +} + +object ExecutionEnv { + + /** create an ExecutionEnv from an execution context only */ + def fromExecutionContext(ec: =>ExecutionContext): ExecutionEnv = + ExecutionEnv( + ExecutorServices.fromExecutionContext(ec), + timeFactor = 1, + retriesFactor = 1) + + def create(arguments: Arguments, systemLogger: Logger, tag: Option[String] = None): ExecutionEnv = + fromGlobalExecutionContext + + def createSpecs2(arguments: Arguments, systemLogger: Logger, tag: Option[String] = None): ExecutionEnv = + fromGlobalExecutionContext + + /** create an ExecutionEnv from Scala global execution context */ + def fromGlobalExecutionContext: ExecutionEnv = + fromExecutionContext(scala.concurrent.ExecutionContext.global) + +} diff --git a/common/native/src/main/scala/org/specs2/concurrent/ExecutorServices.scala b/common/native/src/main/scala/org/specs2/concurrent/ExecutorServices.scala new file mode 100644 index 0000000000..56ad591ded --- /dev/null +++ b/common/native/src/main/scala/org/specs2/concurrent/ExecutorServices.scala @@ -0,0 +1,48 @@ +package org.specs2.concurrent + +import org.specs2.control.eff._ +import scala.concurrent.ExecutionContext +import scala.concurrent.duration.FiniteDuration + +/** + * Executor services for native + * + * The global execution context is used for both + * executing tests and scheduling timeouts + */ +case class ExecutorServices(executionContextEval: Evaluated[ExecutionContext], + schedulerEval: Evaluated[Scheduler]) { + + implicit lazy val executionContext: ExecutionContext = + executionContextEval.value + + implicit lazy val scheduler: Scheduler = + schedulerEval.value + + def shutdownNow(): Unit = + () + + /** convenience method to shutdown the services when the final future has completed */ + def shutdownOnComplete[A](future: scala.concurrent.Future[A]): ExecutorServices = + this + + def schedule(timedout: =>Unit, duration: FiniteDuration): () => Unit = + scheduler.schedule(timedout, duration) + +} + +object ExecutorServices { + + lazy val threadsNb: Int = 1 + lazy val specs2ThreadsNb: Int = 1 + + def fromExecutionContext(ec: =>ExecutionContext): ExecutorServices = + ExecutorServices( + Memoized(ec), + Memoized(Schedulers.default) + ) + + def fromGlobalExecutionContext: ExecutorServices = + fromExecutionContext(scala.concurrent.ExecutionContext.global) + +} diff --git a/common/native/src/main/scala/org/specs2/concurrent/package.scala b/common/native/src/main/scala/org/specs2/concurrent/package.scala new file mode 100644 index 0000000000..2e9b55afbb --- /dev/null +++ b/common/native/src/main/scala/org/specs2/concurrent/package.scala @@ -0,0 +1,13 @@ +package org.specs2 + +import scala.concurrent.{ Await, Awaitable } +import scala.concurrent.duration.Duration + +package object concurrent { + + private[specs2] def awaitResult[A](a: Awaitable[A], d: Duration) = { + scala.scalanative.runtime.loop() + Await.result(a, d) + } + +} diff --git a/common/native/src/main/scala/scala/concurrent/ExecutionContext.scala b/common/native/src/main/scala/scala/concurrent/ExecutionContext.scala deleted file mode 100644 index c28075d281..0000000000 --- a/common/native/src/main/scala/scala/concurrent/ExecutionContext.scala +++ /dev/null @@ -1,182 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ Scala API ** -** / __/ __// _ | / / / _ | (c) 2003-2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** -** /____/\___/_/ |_/____/_/ | | ** -** |/ ** -\* */ - -package scala.concurrent - - -import java.util.concurrent.{ ExecutorService, Executor } -import scala.annotation.implicitNotFound - -/** - * An `ExecutionContext` can execute program logic asynchronously, - * typically but not necessarily on a thread pool. - * - * A general purpose `ExecutionContext` must be asynchronous in executing - * any `Runnable` that is passed into its `execute`-method. A special purpose - * `ExecutionContext` may be synchronous but must only be passed to code that - * is explicitly safe to be run using a synchronously executing `ExecutionContext`. - * - * APIs such as `Future.onComplete` require you to provide a callback - * and an implicit `ExecutionContext`. The implicit `ExecutionContext` - * will be used to execute the callback. - * - * It is possible to simply import - * `scala.concurrent.ExecutionContext.Implicits.global` to obtain an - * implicit `ExecutionContext`. This global context is a reasonable - * default thread pool. - * - * However, application developers should carefully consider where they - * want to set policy; ideally, one place per application (or per - * logically-related section of code) will make a decision about - * which `ExecutionContext` to use. That is, you might want to avoid - * hardcoding `scala.concurrent.ExecutionContext.Implicits.global` all - * over the place in your code. - * One approach is to add `(implicit ec: ExecutionContext)` - * to methods which need an `ExecutionContext`. Then import a specific - * context in one place for the entire application or module, - * passing it implicitly to individual methods. - * - * A custom `ExecutionContext` may be appropriate to execute code - * which blocks on IO or performs long-running computations. - * `ExecutionContext.fromExecutorService` and `ExecutionContext.fromExecutor` - * are good ways to create a custom `ExecutionContext`. - * - * The intent of `ExecutionContext` is to lexically scope code execution. - * That is, each method, class, file, package, or application determines - * how to run its own code. This avoids issues such as running - * application callbacks on a thread pool belonging to a networking library. - * The size of a networking library's thread pool can be safely configured, - * knowing that only that library's network operations will be affected. - * Application callback execution can be configured separately. - */ -@implicitNotFound("""Cannot find an implicit ExecutionContext. You might pass -an (implicit ec: ExecutionContext) parameter to your method -or import scala.concurrent.ExecutionContext.Implicits.global.""") -trait ExecutionContext { - - /** Runs a block of code on this execution context. - * - * @param runnable the task to execute - */ - def execute(runnable: Runnable): Unit - - /** Reports that an asynchronous computation failed. - * - * @param cause the cause of the failure - */ - def reportFailure(@deprecatedName('t) cause: Throwable): Unit - - /** Prepares for the execution of a task. Returns the prepared execution context. - * - * `prepare` should be called at the site where an `ExecutionContext` is received (for - * example, through an implicit method parameter). The returned execution context may - * then be used to execute tasks. The role of `prepare` is to save any context relevant - * to an execution's ''call site'', so that this context may be restored at the - * ''execution site''. (These are often different: for example, execution may be - * suspended through a `Promise`'s future until the `Promise` is completed, which may - * be done in another thread, on another stack.) - * - * Note: a valid implementation of `prepare` is one that simply returns `this`. - * - * @return the prepared execution context - */ - def prepare(): ExecutionContext = this - -} - -/** - * An [[ExecutionContext]] that is also a - * Java [[http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html Executor]]. - */ -trait ExecutionContextExecutor extends ExecutionContext with Executor - -/** - * An [[ExecutionContext]] that is also a - * Java [[http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ExecutorService]]. - */ -trait ExecutionContextExecutorService extends ExecutionContextExecutor with ExecutorService - - -/** Contains factory methods for creating execution contexts. - */ -object ExecutionContext { - /** - * The explicit global `ExecutionContext`. Invoke `global` when you want to provide the global - * `ExecutionContext` explicitly. - * - * The default `ExecutionContext` implementation is backed by a work-stealing thread pool. By default, - * the thread pool uses a target number of worker threads equal to the number of - * [[https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors-- available processors]]. - * - * @return the global `ExecutionContext` - */ - def global: ExecutionContextExecutor = Implicits.global - - object Implicits { - /** - * The implicit global `ExecutionContext`. Import `global` when you want to provide the global - * `ExecutionContext` implicitly. - * - * The default `ExecutionContext` implementation is backed by a work-stealing thread pool. By default, - * the thread pool uses a target number of worker threads equal to the number of - * [[https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors-- available processors]]. - */ - implicit lazy val global: ExecutionContextExecutor = - new ExecutionContextExecutor { - override def execute(runnable: Runnable): Unit = runnable.run() - - override def reportFailure(cause: Throwable): Unit = cause.printStackTrace() - } - } - - /** Creates an `ExecutionContext` from the given `ExecutorService`. - * - * @param e the `ExecutorService` to use. If `null`, a new `ExecutorService` is created with [[http://www.scala-lang.org/api/current/index.html#scala.concurrent.ExecutionContext$@global:scala.concurrent.ExecutionContextExecutor default configuration]]. - * @param reporter a function for error reporting - * @return the `ExecutionContext` using the given `ExecutorService` - */ - def fromExecutorService(e: ExecutorService, reporter: Throwable => Unit): ExecutionContextExecutorService = - impl.ExecutionContextImpl.fromExecutorService(e, reporter) - - /** Creates an `ExecutionContext` from the given `ExecutorService` with the [[scala.concurrent.ExecutionContext$.defaultReporter default reporter]]. - * - * If it is guaranteed that none of the executed tasks are blocking, a single-threaded `ExecutorService` - * can be used to create an `ExecutionContext` as follows: - * - * {{{ - * import java.util.concurrent.Executors - * val ec = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor()) - * }}} - * - * @param e the `ExecutorService` to use. If `null`, a new `ExecutorService` is created with [[http://www.scala-lang.org/api/current/index.html#scala.concurrent.ExecutionContext$@global:scala.concurrent.ExecutionContextExecutor default configuration]]. - * @return the `ExecutionContext` using the given `ExecutorService` - */ - def fromExecutorService(e: ExecutorService): ExecutionContextExecutorService = fromExecutorService(e, defaultReporter) - - /** Creates an `ExecutionContext` from the given `Executor`. - * - * @param e the `Executor` to use. If `null`, a new `Executor` is created with [[http://www.scala-lang.org/api/current/index.html#scala.concurrent.ExecutionContext$@global:scala.concurrent.ExecutionContextExecutor default configuration]]. - * @param reporter a function for error reporting - * @return the `ExecutionContext` using the given `Executor` - */ - def fromExecutor(e: Executor, reporter: Throwable => Unit): ExecutionContextExecutor = - impl.ExecutionContextImpl.fromExecutor(e, reporter) - - /** Creates an `ExecutionContext` from the given `Executor` with the [[scala.concurrent.ExecutionContext$.defaultReporter default reporter]]. - * - * @param e the `Executor` to use. If `null`, a new `Executor` is created with [[http://www.scala-lang.org/api/current/index.html#scala.concurrent.ExecutionContext$@global:scala.concurrent.ExecutionContextExecutor default configuration]]. - * @return the `ExecutionContext` using the given `Executor` - */ - def fromExecutor(e: Executor): ExecutionContextExecutor = fromExecutor(e, defaultReporter) - - /** The default reporter simply prints the stack trace of the `Throwable` to [[http://docs.oracle.com/javase/8/docs/api/java/lang/System.html#err System.err]]. - * - * @return the function for error reporting - */ - def defaultReporter: Throwable => Unit = _.printStackTrace() -} diff --git a/common/shared/src/main/scala/org/specs2/reflect/ClassName.scala b/common/shared/src/main/scala/org/specs2/reflect/ClassName.scala index fe439a9030..66a6df730e 100644 --- a/common/shared/src/main/scala/org/specs2/reflect/ClassName.scala +++ b/common/shared/src/main/scala/org/specs2/reflect/ClassName.scala @@ -65,7 +65,8 @@ trait ClassName { outer => else name }(klass.getName) - if (result.contains("anon") && klass.getSuperclass != null) simpleName(klass.getSuperclass) + if (Platform.isNative) result + else if (result.contains("anon") && klass.getSuperclass != null) simpleName(klass.getSuperclass) else result } @@ -74,6 +75,7 @@ trait ClassName { outer => */ def humanName(c: Class[_]): String = { val name = simpleName(c) + if (Platform.isNative) name.camelCaseToWords if (name.contains("$") && c.getSuperclass != null) humanName(c.getSuperclass) else name.camelCaseToWords } diff --git a/core/js-native/src/main/scala/org/specs2/control/ExecuteActions.scala b/core/js/src/main/scala/org/specs2/control/ExecuteActions.scala similarity index 100% rename from core/js-native/src/main/scala/org/specs2/control/ExecuteActions.scala rename to core/js/src/main/scala/org/specs2/control/ExecuteActions.scala diff --git a/core/native/src/main/scala/org/specs2/control/ExecuteActions.scala b/core/native/src/main/scala/org/specs2/control/ExecuteActions.scala new file mode 100644 index 0000000000..2ba82af315 --- /dev/null +++ b/core/native/src/main/scala/org/specs2/control/ExecuteActions.scala @@ -0,0 +1,80 @@ +package org.specs2.control + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.control.eff.ErrorEffect._ +import org.specs2.control.eff.syntax.safe._ +import org.specs2.control.eff.syntax.error._ +import org.specs2.control.eff.syntax.console._ +import org.specs2.control.eff.syntax.warnings._ +import org.specs2.control.eff.syntax.future._ +import org.specs2.execute.{AsResult, Result} +import org.specs2.fp.Monoid + +import scala.concurrent._ + +trait ExecuteActions { + + def executeAction[A](action: Action[A], printer: String => Unit = s => ())(ee: ExecutionEnv): (Error Either A, List[String]) = + throw new Exception("executeAction not implemented") + + def executeActionFuture[A](action: Action[A], printer: String => Unit = s => ())(ee: ExecutionEnv): Future[(Error Either A, List[String])] = { + implicit val es = ee.executorServices + implicit val executionContext = ee.executionContext + + action.runError.runConsoleToPrinter(printer).execSafe.runWarnings.runAsync.map { + case (Left(t), ws) => (Left(Left(t)), ws) + case (Right(e), ws) => (e, ws) + } + } + + def runActionFuture[A](action: Action[A], printer: String => Unit = s => ())(ee: ExecutionEnv): Future[A] = { + implicit val executionContext = ee.executionContext + + executeActionFuture(action, printer)(ee).map(_._1).flatMap { + case Left(Left(t)) => Future.failed(t) + case Left(Right(s)) => Future.failed(new Exception(s)) + case Right(a) => Future.successful(a) + } + } + + def runAction[A](action: Action[A], printer: String => Unit = s => ())(ee: ExecutionEnv): Error Either A = + throw new Exception("runAction not implemented") + + def attemptExecuteAction[A](action: Action[A], printer: String => Unit = s => ())(ee: ExecutionEnv): Throwable Either (Error Either A, List[String]) = + throw new Exception("attemptExecuteAction not implemented") + + /** + * This implicit allows an Action[result] to be used inside an example. + * + * For example to read a database. + */ + implicit def actionAsResult[T](implicit r: AsResult[T], ee: ExecutionEnv): AsResult[Action[T]] = new AsResult[Action[T]] { + def asResult(action: =>Action[T]): Result = + runAction(action)(ee).fold( + err => err.fold(t => org.specs2.execute.Error(t), f => org.specs2.execute.Failure(f)), + ok => AsResult(ok) + ) + } + + implicit class ActionRunOps[T](action: Action[T]) { + def run(ee: ExecutionEnv)(implicit m: Monoid[T]): T = + action.runOption(ee).getOrElse(m.zero) + + def runFuture(ee: ExecutionEnv): Future[T] = + runActionFuture(action)(ee) + + def runOption(ee: ExecutionEnv): Option[T] = + runAction(action, println)(ee) match { + case Right(a) => Option(a) + case Left(t) => println("error while interpreting an action "+t.fold(Throwables.render, f => f)); None + } + + def runEither(ee: ExecutionEnv): Either[Error, T] = + runAction(action, println)(ee) + } + + +} + +object ExecuteActions extends ExecuteActions + diff --git a/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala b/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala index 02881aa730..a231bf4659 100644 --- a/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala +++ b/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala @@ -15,6 +15,7 @@ import Actions._ import org.specs2.reflect._ import org.specs2.control.eff.ErrorEffect._ import org.specs2.concurrent.ExecutionEnv +import org.specs2.concurrent.awaitResult import org.specs2.control.ExecuteActions._ import org.specs2.data.NamedTag @@ -159,7 +160,7 @@ case class SbtTask(aTaskDef: TaskDef, env: Env, loader: ClassLoader) extends sbt } def execute(handler: EventHandler, loggers: Array[Logger]): Array[Task] = { - Await.result(executeFuture(handler, loggers), Duration.Inf) + awaitResult(executeFuture(handler, loggers), Duration.Inf) Array() } diff --git a/matcher/native/src/main/scala/org/specs2/matcher/Matchers.scala b/matcher/native/src/main/scala/org/specs2/matcher/Matchers.scala new file mode 100644 index 0000000000..1bda06b8ae --- /dev/null +++ b/matcher/native/src/main/scala/org/specs2/matcher/Matchers.scala @@ -0,0 +1,51 @@ +package org.specs2 +package matcher + +import control.LanguageFeatures + +/** + * Trait aggregating the most common specs2 matchers + */ +trait Matchers extends AnyMatchers + with BeHaveMatchers + with TraversableMatchers + with MapMatchers + with StringMatchers + with ExceptionMatchers + with NumericMatchers + with OptionMatchers + with EitherMatchers + with TryMatchers + with EventuallyMatchers + with MatchersImplicits + with ValueChecks + with LanguageFeatures + +trait Matchers1 extends + AnyBaseMatchers + with TraversableBaseMatchers + with StringBaseMatchers + with ExceptionBaseMatchers + with NumericBaseMatchers + with OptionBaseMatchers + with EitherBaseMatchers + with TryBaseMatchers + with EventuallyMatchers + with ValueChecksBase + +object Matchers extends Matchers + +trait MustMatchers extends Matchers with MustExpectations +object MustMatchers extends MustMatchers with NoMatchResultStackTrace + +private[specs2] trait MustMatchers1 extends Matchers1 with MustExpectations1 +private[specs2] trait MustThrownMatchers1 extends Matchers1 with MustThrownExpectations1 + +trait ShouldMatchers extends Matchers with ShouldExpectations +object ShouldMatchers extends ShouldMatchers with NoMatchResultStackTrace + +trait MustThrownMatchers extends Matchers with MustThrownExpectations +object MustThrownMatchers extends MustThrownMatchers with NoMatchResultStackTrace + +trait ShouldThrownMatchers extends Matchers with ShouldThrownExpectations +object ShouldThrownMatchers extends ShouldThrownMatchers with NoMatchResultStackTrace diff --git a/project/build.properties b/project/build.properties index 9edb75b77c..c8fcab543a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.5.4 +sbt.version=1.6.2 diff --git a/project/depends.scala b/project/depends.scala index 269fdd9fce..6fcdfb72a6 100644 --- a/project/depends.scala +++ b/project/depends.scala @@ -35,16 +35,16 @@ object depends { Seq(libraryDependencies += "org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0") def nativeTest = - Seq(libraryDependencies += "org.scala-native" %%% "test-interface" % nativeVersion) + Seq(libraryDependencies ++= Seq( + "org.scala-native" %%% "test-interface" % nativeVersion, + "org.portable-scala" %%% "portable-scala-reflect" % "1.1.1" + )) def scalaParser = Def.setting { Seq("org.scala-lang.modules" %%% "scala-parser-combinators" % "1.1.2") } def scalaParserNative = Def.setting { - if(nativeVersion == "0.4.0") - Seq("com.github.lolgab" %%% "scala-parser-combinators" % "1.1.2") - else - scalaParser.value + Seq("org.scala-lang.modules" %%% "scala-parser-combinators" % "2.1.0") } def scalaXml = "org.scala-lang.modules" %% "scala-xml" % "1.3.0" diff --git a/project/plugins.sbt b/project/plugins.sbt index 17f3301e80..31f3862420 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,12 +1,12 @@ val scalaJSVersion = Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.7.0") val scalaNativeVersion = -Option(System.getenv("SCALANATIVE_VERSION")).getOrElse("0.4.0") +Option(System.getenv("SCALANATIVE_VERSION")).getOrElse("0.4.3") addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) addSbtPlugin("org.scala-native" % "sbt-scala-native" % scalaNativeVersion) -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.1.0") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") From 4d27bbd06383bda7c4c1bbd3b5e29b4e8e339915 Mon Sep 17 00:00:00 2001 From: etorreborre Date: Thu, 17 Feb 2022 11:42:01 +0100 Subject: [PATCH 09/11] project: published 4.14.0 --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index ea84465234..1d557408a4 100644 --- a/version.sbt +++ b/version.sbt @@ -1,2 +1,2 @@ -ThisBuild / version := "4.13.3" +ThisBuild / version := "4.14.0" ThisBuild / versionScheme := Some("semver-spec") From 57435b4d3ed8cc0c6206b3e253324400b20a447a Mon Sep 17 00:00:00 2001 From: etorreborre Date: Wed, 23 Feb 2022 11:54:16 +0100 Subject: [PATCH 10/11] fix: port the json matchers fix for #1069 --- .../src/main/scala/org/specs2/json/Json.scala | 34 ++++++++++++------- .../org/specs2/matcher/JsonMatchersSpec.scala | 21 +++++++++--- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/matcher-extra/shared/src/main/scala/org/specs2/json/Json.scala b/matcher-extra/shared/src/main/scala/org/specs2/json/Json.scala index 038e0393dc..13f067a78d 100644 --- a/matcher-extra/shared/src/main/scala/org/specs2/json/Json.scala +++ b/matcher-extra/shared/src/main/scala/org/specs2/json/Json.scala @@ -11,22 +11,30 @@ trait Json { * otherwise it will not be thread (see issue #70) * @return Some(json) if the string is parsable as a JSON document */ - def parse(s: String): Option[JSONType] = { - val parser = new Parser { - def parseRaw(input : String) : Option[JSONType] = - phrase(root)(new lexical.Scanner(input)) match { - case Success(result, _) => Some(result) - case _ => None + def parse(s: String): Option[JSONType] = + parseEither(s).toOption + + def parseEither(s: String): Either[String, JSONType] = { + def parseRaw(p: Parser, input: String): Either[String, JSONType] = + (p.phrase(p.root)(new p.lexical.Scanner(input)): @unchecked) match { + case p.Success(result, _) => Right(result) + case p.NoSuccess(e, _) => Left(e) } - } + + val parser = new Parser // give the parser a chance to parse singly-quoted json - parser.parseRaw(s).orElse(if (s.contains("'")) parser.parseRaw(s.replace("'", "\"")) else None) + parseRaw(parser, s) match { + case Right(r) => + Right(r) + case Left(e) => + if (s.contains("'")) parseRaw(parser, s.replace("'", "\"")) + else Left(e) + } } /** show JSON objects with null values shown as 'null' */ - def showJson(a: Any): String = a match { - case map: Map[_, _] => map.map { case (key, value) => s""""$key":${showJsonValue(value)}"""}.mkString("{", ",", "}") + case map: Map[_, _] => map.map { case (key, value) => s""""${quoteDoubleQuote(key.toString)}":${showJsonValue(value)}"""}.mkString("{", ",", "}") case (key, value) => s"""{"$key":${showJsonValue(value)}}""" case JSONObject(map) => showJson(map) case JSONArray(list) => list.map(showJsonValue).mkString("[", ",", "]") @@ -43,12 +51,14 @@ trait Json { */ def showJsonValue(a: Any): String = a match { case null => "null" - case s: String => "\""+s+"\"" + case s: String => "\""+quoteDoubleQuote(s)+"\"" case d: Double => d.toString case b: Boolean => b.toString case other => showJson(other) } - + + def quoteDoubleQuote(s: String): String = + s.replace("\"", "\\\"") } private[specs2] object Json extends Json diff --git a/tests/src/test/scala/org/specs2/matcher/JsonMatchersSpec.scala b/tests/src/test/scala/org/specs2/matcher/JsonMatchersSpec.scala index ec99679081..532d7a806f 100644 --- a/tests/src/test/scala/org/specs2/matcher/JsonMatchersSpec.scala +++ b/tests/src/test/scala/org/specs2/matcher/JsonMatchersSpec.scala @@ -60,15 +60,15 @@ class JsonMatchersSpec extends Specification with JsonMatchers { def is = s2""" The / matcher can be chained with */ ${ "{'person' : {'address' : {'street' : 'here'}}}" must /("person") */("street") /("here") } - - The /#(i) matcher matches the i-th element in an Array + + The /#(i) matcher matches the i-th element in an Array ${ "['name', 'Joe']" must /#(1) /("Joe") } ${ "['name', 'Joe']" must not /#(1) /("M.*.r") } ${ "{'person' : ['name', 'Joe'] }" must /("person") /#(1) /("Joe") } ${ "{'person' : ['name', ['Joe', 'Moe']] }" must /("person") /#(1) /#(1) /("Moe") } ${ "{'house' : {'person' : ['name', 'Joe']}}" must */("person") /#(1) /("Joe") } - - The /#(i) matcher matches the i-th element in a Map + + The /#(i) matcher matches the i-th element in a Map ${ "{'name' : 'Joe', 'name2' : 'Moe'}" must /#(1) /("name2" -> "Moe") } ${ "{'person' : {'name': 'Joe', 'name2' : 'Moe'} }" must /("person") /#(1) /("name2" -> "Moe") } ${ "{'house' : {'person' : {'name': 'Joe', 'name2' : 'Moe'}}}" must */("person") /#(1) /("name2" -> "Moe") } @@ -92,6 +92,19 @@ class JsonMatchersSpec extends Specification with JsonMatchers { def is = s2""" Matchers must be resilient when there are null values ${ """{ "b" : { "a" : 2, "c" : null } }""" must /("b" -> /("a" -> 2)) } + Parsing double quotes + in a value ${raw"""{"a": "hello\"world"}""" must /("a" -> """hello"world""")} + + in a value in an array ${raw"""[{"a": "hello\"world"}]""" must */("a" -> """hello"world""")} + + in a nested key ${raw"""{"values": [{"hello\"world" : "a"}]}""" must /("values").andHave( + contain(/("""hello"world""" -> "a")) + )} + + in a nested value ${raw"""{"values": [{"a": "hello\"world"}]}""" must /("values").andHave( + contain(/("a" -> """hello"world""")) + )} + """ def andHave = { From d428305a84febaa298c1a12e8080a181bf8b3f3b Mon Sep 17 00:00:00 2001 From: etorreborre Date: Wed, 23 Feb 2022 11:54:55 +0100 Subject: [PATCH 11/11] project: release 4.14.1 --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 1d557408a4..04af15af1c 100644 --- a/version.sbt +++ b/version.sbt @@ -1,2 +1,2 @@ -ThisBuild / version := "4.14.0" +ThisBuild / version := "4.14.1" ThisBuild / versionScheme := Some("semver-spec")