All Downloads are FREE. Search and download functionalities are using the official Maven repository.

weaver.Runner.scala Maven / Gradle / Ivy

The newest version!
package weaver

import cats.Monoid
import cats.data.Chain
import cats.effect.std.Queue
import cats.effect.{ Async, Ref }
import cats.syntax.all._

import TestOutcome.{ Summary, Verbose }
import Colours._

class Runner[F[_]: Async](
    args: List[String],
    maxConcurrentSuites: Int)(
    printLine: String => F[Unit]) {

  import Runner._

  // Signaling option, because we need to detect completion
  private type Channel[A] = Queue[F, Option[A]]

  def run(suites: fs2.Stream[F, Suite[F]]): F[Outcome] =
    for {
      buffer  <- Ref[F].of(Chain.empty[SpecEvent])
      channel <- Queue.unbounded[F, Option[SpecEvent]]
      outcome <-
        Async[F].background(consume(channel, buffer)).use { outcome =>
          val res = outcome.flatMap(_.embed(onCancel = Outcome.empty.pure[F]))

          suites
            .parEvalMap(math.max(1, maxConcurrentSuites)) { suite =>
              suite
                .spec(args)
                .compile
                .toList
                .map(SpecEvent(suite.name, _))
                .flatMap(produce(channel))
            }
            .compile
            .drain *> complete(channel) *> res
        }
    } yield outcome

  private def produce(ch: Channel[SpecEvent])(event: SpecEvent): F[Unit] =
    ch.offer(Some(event))

  private def complete(channel: Channel[SpecEvent]): F[Unit] =
    channel.offer(None) // We are done !

  // Recursively consumes from a channel until a "None" gets produced,
  // indicating the end of the stream.
  private def consume(
      ch: Channel[SpecEvent],
      buffer: Ref[F, Chain[SpecEvent]]): F[Outcome] = {

    val stars = "*************"

    def newLine = printLine("")
    def printTestEvent(mode: TestOutcome.Mode)(event: TestOutcome) =
      printLine(event.formatted(mode))
    def handle(specEvent: SpecEvent): F[Outcome] = {
      val (successes, failures, outcome) =
        // format: off
        specEvent.events.foldMap[(List[TestOutcome],List[TestOutcome],Outcome)] {
          case ev if ev.status.isFailed => (List.empty, List(ev), Outcome.fromEvent(ev))
          case ev => (List(ev), List.empty, Outcome.fromEvent(ev))
        }
        // format: on

      for {
        _ <- printLine(cyan(specEvent.name))
        _ <- (successes ++ failures).traverse(printTestEvent(Summary))
        _ <- newLine
        _ <- buffer
          .update(_.append(specEvent.copy(events = failures)))
          .whenA(failures.nonEmpty)
      } yield outcome
    }

    fs2.Stream.repeatEval(ch.take)
      .unNoneTerminate.evalMap(handle)
      .compile.foldMonoid.flatMap {
        outcome =>
          for {
            failures <- buffer.get
            _ <- (printLine(red(stars) + "FAILURES" + red(stars)) *> failures
              .traverse[F, Unit] { specEvent =>
                printLine(cyan(specEvent.name)) *>
                  specEvent.events.traverse(printTestEvent(Verbose)) *>
                  newLine
              }
              .void).whenA(failures.nonEmpty)
            _ <- printLine(outcome.formatted)
          } yield outcome
      }
  }

}

object Runner {

  case class SpecEvent(name: String, events: List[TestOutcome])

  case class Outcome(
      successes: Int,
      ignored: Int,
      cancelled: Int,
      failures: Int) { self =>

    def total = successes + ignored + cancelled + failures

    def formatted: String =
      s"Total $total, Failed $failures, Passed $successes, Ignored $ignored, Cancelled $cancelled"

  }

  object Outcome {
    val empty = Outcome(0, 0, 0, 0)

    def fromEvent(event: TestOutcome): Outcome = event.status match {
      case TestStatus.Exception =>
        Outcome(0, 0, 0, failures = 1)
      case TestStatus.Failure =>
        Outcome(0, 0, 0, failures = 1)
      case TestStatus.Success =>
        Outcome(successes = 1, 0, 0, 0)
      case TestStatus.Ignored =>
        Outcome(0, ignored = 1, 0, 0)
      case TestStatus.Cancelled =>
        Outcome(0, 0, cancelled = 1, 0)
    }

    implicit val monoid: Monoid[Outcome] = new Monoid[Outcome] {
      override def empty = Outcome.empty

      override def combine(left: Outcome, right: Outcome) = Outcome(
        left.successes + right.successes,
        left.ignored + right.ignored,
        left.cancelled + right.cancelled,
        left.failures + right.failures
      )
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy