From 846d0a4f220396aa4f5c19f5fe09e16a323a483f Mon Sep 17 00:00:00 2001 From: etorreborre Date: Sat, 12 Oct 2024 16:38:56 +0200 Subject: [PATCH] fix: make the NaturalTransformation lazy in order to not duplicate effects for Id ~> Action --- build.sbt | 8 +++++- .../scala/org/specs2/control/Operation.scala | 2 +- .../test/scala/org/specs2/fp/FoldSpec.scala | 27 +++++++++++++++++++ .../org/specs2/fp/NaturalTransformation.scala | 4 +-- .../org/specs2/reporter/HtmlPrinter.scala | 5 ++-- 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 core/jvm/src/test/scala/org/specs2/fp/FoldSpec.scala diff --git a/build.sbt b/build.sbt index ff98febe6f..9d362d2223 100644 --- a/build.sbt +++ b/build.sbt @@ -120,7 +120,13 @@ lazy val mimaSettings = // Because, in that case, it is very possible to confuse the ownEnv with the env and shutdown the env // which breaks the execution of the whole specification ProblemFilters.exclude[DirectMissingMethodProblem]("org.specs2.specification.core.OwnEnv.ownEnv"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.specs2.specification.core.OwnExecutionEnv.env") + ProblemFilters.exclude[DirectMissingMethodProblem]("org.specs2.specification.core.OwnExecutionEnv.env"), + + // issue #1277 NaturalTransformation needs to be made lazier + ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.specs2.fp.NaturalTransformation.apply"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("org.specs2.fp.NaturalTransformation.apply"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.specs2.fp.NaturalTransformation#naturalId.apply"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.specs2.control.Operation#operationToAction.apply") ) ) diff --git a/common/shared/src/main/scala/org/specs2/control/Operation.scala b/common/shared/src/main/scala/org/specs2/control/Operation.scala index 4b0d46b6bf..312f41dd6c 100644 --- a/common/shared/src/main/scala/org/specs2/control/Operation.scala +++ b/common/shared/src/main/scala/org/specs2/control/Operation.scala @@ -163,7 +163,7 @@ object Operation: "Applicative[Operation]" given operationToAction: NaturalTransformation[Operation, Action] with - def apply[A](operation: Operation[A]): Action[A] = + def apply[A](operation: =>Operation[A]): Action[A] = operation.toAction given SafeOperation: Safe[Operation] with diff --git a/core/jvm/src/test/scala/org/specs2/fp/FoldSpec.scala b/core/jvm/src/test/scala/org/specs2/fp/FoldSpec.scala new file mode 100644 index 0000000000..99ebd1d711 --- /dev/null +++ b/core/jvm/src/test/scala/org/specs2/fp/FoldSpec.scala @@ -0,0 +1,27 @@ +package user + +import org.specs2.* +import org.specs2.concurrent.* +import org.specs2.control.* +import org.specs2.control.origami.* +import org.specs2.control.producer.* +import org.specs2.fp.* + +class FoldSpec(ee: ExecutionEnv) extends Specification { + def is = s2""" + + A correct implementation of NaturalTransformation from Id to Action must not duplicate effects (see issue #1277) $dontDuplicateEffects + +""" + + def dontDuplicateEffects = { + val p = Producer.emitAll[Action, Int](1, 2, 3) + + // The NaturalTransformation from Id (which is Id[X] X) to Action must + // make sure to not run side effects of X twice. + // This tests guarantees that during the evaluation of `zip` and the various folds + // we don't run the action for collection elements into a mutable map more than necessary. + val vs = p.fold(Folds.list[Int].into[Action] `zip` Folds.list[Int].into[Action]) + vs.run(ee) === (List(1, 2, 3), List(1, 2, 3)) + } +} diff --git a/fp/src/main/scala/org/specs2/fp/NaturalTransformation.scala b/fp/src/main/scala/org/specs2/fp/NaturalTransformation.scala index 8fa5d7f046..0a0f0713f2 100644 --- a/fp/src/main/scala/org/specs2/fp/NaturalTransformation.scala +++ b/fp/src/main/scala/org/specs2/fp/NaturalTransformation.scala @@ -1,10 +1,10 @@ package org.specs2.fp trait NaturalTransformation[-F[_], +G[_]]: - def apply[A](fa: F[A]): G[A] + def apply[A](fa: =>F[A]): G[A] object NaturalTransformation: given naturalId[M[_]: Monad]: NaturalTransformation[Id, M] with - def apply[A](fa: Id[A]): M[A] = + def apply[A](fa: =>Id[A]): M[A] = summon[Monad[M]].point(fa) diff --git a/html/src/main/scala/org/specs2/reporter/HtmlPrinter.scala b/html/src/main/scala/org/specs2/reporter/HtmlPrinter.scala index 95024ce625..f12de38610 100644 --- a/html/src/main/scala/org/specs2/reporter/HtmlPrinter.scala +++ b/html/src/main/scala/org/specs2/reporter/HtmlPrinter.scala @@ -40,8 +40,8 @@ case class HtmlPrinter(env: Env, searchPage: SearchPage, logger: Logger = Consol /** @return a SinkTask for the Html output */ def sink(spec: SpecStructure): AsyncSink[Fragment] = - ((Statistics.fold `zip` list[Fragment].into[Action] `zip` SimpleTimer.timerFold.into[Action]) <* - fromStart((getHtmlOptions(env.arguments) >>= (options => copyResources(env, options))).void.toAction)) + val copyHtmlResources = (getHtmlOptions(env.arguments) >>= (options => copyResources(env, options))).void.toAction + val htmlSink = (Statistics.fold `zip` list[Fragment].into[Action] `zip` SimpleTimer.timerFold.into[Action]) .mapFlatten { case ((stats, fragments), timer) => val executedSpec = spec.copy(lazyFragments = () => Fragments(fragments*)) getPandoc(env).flatMap { @@ -49,6 +49,7 @@ case class HtmlPrinter(env: Env, searchPage: SearchPage, logger: Logger = Consol case _ => printHtml(env, executedSpec, stats, timer).toAction } } + htmlSink.startWith(copyHtmlResources) /** WITHOUT PANDOC */