Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: ability to run in parallel, test summary at the end #415

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions bleep-cli/src/scala/bleep/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ object Main {
.orNone

val watch = Opts.flag("watch", "start in watch mode", "w").orFalse
val parallelism = Opts.option[Int]("parallelim", "parallelism", "p").withDefault(1)

lazy val ret: Opts[BleepBuildCommand] = {
val allCommands = List(
Expand Down Expand Up @@ -188,9 +189,7 @@ object Main {
(watch, hasSourcegenProjectNames).mapN { case (watch, projectNames) => commands.SourceGen(watch, projectNames) }
),
Opts.subcommand("test", "test projects")(
(watch, testProjectNames, testSuitesOnly, testSuitesExclude).mapN { case (watch, projectNames, testSuitesOnly, testSuitesExclude) =>
commands.Test(watch, projectNames, testSuitesOnly, testSuitesExclude)
}
(watch, testProjectNames, testSuitesOnly, testSuitesExclude, parallelism).mapN(commands.Test.apply)
),
Opts.subcommand("list-tests", "list tests in projects")(
testProjectNames.map { projectNames =>
Expand Down
5 changes: 3 additions & 2 deletions bleep-core/src/scala/bleep/Commands.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ class Commands(started: Started) {
projects: List[model.CrossProjectName],
watch: Boolean = false,
testOnlyClasses: Option[NonEmptyList[String]],
testExcludeClasses: Option[NonEmptyList[String]]
testExcludeClasses: Option[NonEmptyList[String]],
parallelism: Int = 1
): Unit =
force(commands.Test(watch, projects.toArray, testOnlyClasses, testExcludeClasses))
force(commands.Test(watch, projects.toArray, testOnlyClasses, testExcludeClasses, parallelism))

def script(name: model.ScriptName, args: List[String], watch: Boolean = false): Unit =
force(commands.Script(name, args, watch))
Expand Down
94 changes: 66 additions & 28 deletions bleep-core/src/scala/bleep/commands/Test.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package bleep
package commands

import bleep.bsp.BspCommandFailed
import bleep.internal.{DoSourceGen, TransitiveProjects}
import bleep.logging.Logger
import bloop.rifle.BuildServer
import cats.data.NonEmptyList
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import ch.epfl.scala.bsp4j
import ch.epfl.scala.bsp4j.ScalaTestClassesParams

import java.util
import scala.jdk.CollectionConverters.*
Expand All @@ -14,7 +17,8 @@ case class Test(
watch: Boolean,
projects: Array[model.CrossProjectName],
testSuitesOnly: Option[NonEmptyList[String]],
testSuitesExclude: Option[NonEmptyList[String]]
testSuitesExclude: Option[NonEmptyList[String]],
parallelism: Int
) extends BleepCommandRemote(watch)
with BleepCommandRemote.OnlyChanged {

Expand All @@ -24,37 +28,71 @@ case class Test(
override def onlyChangedProjects(started: Started, isChanged: model.CrossProjectName => Boolean): BleepCommandRemote =
copy(projects = watchableProjects(started).transitiveFilter(isChanged).direct)

override def runWithServer(started: Started, bloop: BuildServer): Either[BleepException, Unit] =
DoSourceGen(started, bloop, watchableProjects(started)).flatMap { _ =>
val allTargets = BleepCommandRemote.buildTargets(started.buildPaths, projects)

val maybeTestParams: Either[BleepException, bsp4j.TestParams] =
(testSuitesExclude, testSuitesOnly) match {
case (None, None) =>
Right(new bsp4j.TestParams(allTargets))
case _ =>
val allTestSuitesResult: bsp4j.ScalaTestClassesResult =
bloop.buildTargetScalaTestClasses(new bsp4j.ScalaTestClassesParams(allTargets)).get()

Test
.testParamsWithFilter(allTestSuitesResult, testSuitesOnly, testSuitesExclude)
.toRight(Test.NoTestSuitesFound)
}
override def runWithServer(started: Started, bloop: BuildServer): Either[BleepException, Unit] = {
val ioSuccess = for {
_ <- IO.fromEither(DoSourceGen(started, bloop, watchableProjects(started)))
// rough estimate to make it a bit more probable that we start to run tests before everything is compiled
sortedProjects = projects.sortBy(name => started.build.resolvedDependsOn.get(name).fold(0)(_.size))
testResults <-
fs2.Stream
.emits(sortedProjects)
.covary[IO]
.parEvalMap(parallelism) { projectName =>
val target = BleepCommandRemote.buildTarget(started.buildPaths, projectName)

val maybeTestParams = (testSuitesExclude, testSuitesOnly) match {
case (None, None) =>
IO.pure(Some(new bsp4j.TestParams(util.List.of(target))))
case _ =>
val params = new ScalaTestClassesParams(util.List.of(target))
IO.fromCompletableFuture(IO.delay(bloop.buildTargetScalaTestClasses(params)))
.map(testSuiteResult => Test.testParamsWithFilter(testSuiteResult, testSuitesOnly, testSuitesExclude))
}

maybeTestParams
.flatMap {
case None => IO.pure(None)
case Some(testParams) => IO.fromCompletableFuture(IO(bloop.buildTargetTest(testParams))).map(Some.apply)
}
.attempt
.map(res => (projectName, res))
}
.compile
.toList
res <- Test.printAndCheck(started.logger, testResults)
} yield res

if (ioSuccess.unsafeRunSync()) Right(()) else Left(Test.TestsFailed)
}
}

maybeTestParams.flatMap { testParams =>
bloop.buildTargetTest(testParams).get().getStatusCode match {
object Test {
def printAndCheck(logger: Logger, results: List[(model.CrossProjectName, Either[Throwable, Option[bsp4j.TestResult]])]): IO[Boolean] = IO {
var success = true
logger.info("---------------------------")
logger.info("Bleep test project summary:")
results.foreach {
case (name, Right(None)) =>
logger.warn(s"$name (no tests picked)")
case (name, Right(Some(res))) =>
res.getStatusCode match {
case bsp4j.StatusCode.OK =>
started.logger.info("Tests succeeded")
Right(())
case errorCode =>
Left(new BspCommandFailed("tests", projects, BspCommandFailed.StatusCode(errorCode)))
logger.info(s"✅ $name")
case bsp4j.StatusCode.ERROR =>
success = false
logger.error(s"❌ $name")
case bsp4j.StatusCode.CANCELLED =>
success = false
logger.warn(s"⚠️$name")
}
}
case (name, Left(th)) =>
success = false
logger.error(s"💣 $name", th)
}
}
success
}

object Test {
object NoTestSuitesFound extends BleepException(s"No tests found in projects")
object TestsFailed extends BleepException(s"Tests failed")

private implicit class MapSyntax[K, V](m: Map[K, List[V]]) {
// use this to avoid asking bloop to run tests for projects we know we won't run tests from
Expand Down
8 changes: 4 additions & 4 deletions bleep-tests/src/scala/bleep/CliInvocationTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ class CliInvocationTest extends AnyFunSuite {
val systemOut = System.out
val systemErr = System.err

val stdOutBuffer = ByteArrayOutputStream()
val stdErrBuffer = ByteArrayOutputStream()
val bufferedOut = PrintStream(stdOutBuffer)
val bufferedErr = PrintStream(stdErrBuffer)
val stdOutBuffer = new ByteArrayOutputStream()
val stdErrBuffer = new ByteArrayOutputStream()
val bufferedOut = new PrintStream(stdOutBuffer)
val bufferedErr = new PrintStream(stdErrBuffer)

System.setOut(bufferedOut)
System.setErr(bufferedErr)
Expand Down
1 change: 1 addition & 0 deletions bleep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ projects:
mainClass: bleep.Main
bleep-core:
dependencies:
- co.fs2::fs2-core:3.10.2
- for3Use213: true
module: ch.epfl.scala::bloop-config:2.0.3
- for3Use213: true
Expand Down
Loading