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

Provide information to Suite about other discovered Suites #2306

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 40 additions & 9 deletions js/core/src/main/scala/org/scalatest/tools/MasterRunner.scala
Expand Up @@ -15,7 +15,7 @@
*/
package org.scalatest.tools

import org.scalatest.Tracker
import org.scalatest.{RunningSuite, Tracker}
import org.scalatest.events.Summary
import sbt.testing.{Framework => BaseFramework, Event => SbtEvent, Status => SbtStatus, _}

Expand Down Expand Up @@ -127,7 +127,10 @@ class MasterRunner(theArgs: Array[String], theRemoteArgs: Array[String], testCla
val tracker = new Tracker
val summaryCounter = new SummaryCounter

var knownSuites: Map[(String, List[Selector]), TaskRunner] = Map.empty

def done(): String = {
knownSuites = Map.empty
val duration = Platform.currentTime - runStartTime
val summary = new Summary(summaryCounter.testsSucceededCount, summaryCounter.testsFailedCount, summaryCounter.testsIgnoredCount, summaryCounter.testsPendingCount,
summaryCounter.testsCanceledCount, summaryCounter.suitesCompletedCount, summaryCounter.suitesAbortedCount, summaryCounter.scopesPendingCount)
Expand Down Expand Up @@ -164,11 +167,23 @@ class MasterRunner(theArgs: Array[String], theRemoteArgs: Array[String], testCla
paths.exists(path => td.fullyQualifiedName.startsWith(path) && td.fullyQualifiedName.substring(path.length).lastIndexOf('.') <= 0)
}

def createTask(t: TaskDef): Task =
new TaskRunner(t, testClassLoader, tracker, tagsToInclude, tagsToExclude, t.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
def createTask(allRunningSuites: () => List[RunningSuite], t: TaskDef): TaskRunner =
new TaskRunner(t, allRunningSuites, true, testClassLoader, tracker, tagsToInclude, tagsToExclude, t.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
presentReminderWithShortStackTraces, presentReminderWithFullStackTraces, presentReminderWithoutCanceledTests, presentFilePathname, presentJson, Some(send))

(if (wildcard.isEmpty && membersOnly.isEmpty) taskDefs else (filterWildcard(wildcard, taskDefs) ++ filterMembersOnly(membersOnly, taskDefs)).distinct).map(createTask)
val chosenTaskDefs = if (wildcard.isEmpty && membersOnly.isEmpty) taskDefs else (filterWildcard(wildcard, taskDefs) ++ filterMembersOnly(membersOnly, taskDefs)).distinct

lazy val taskRunners: Array[TaskRunner] = {
val allRunningSuites = () => taskRunners.map(_.runningSuite).toList
chosenTaskDefs.map(createTask(allRunningSuites, _))
}

knownSuites ++= taskRunners.map { t =>
val taskDef = t.taskDef()
(taskDef.fullyQualifiedName() -> taskDef.selectors().toList) -> t
}.toMap

taskRunners.toArray[Task]
}

private def send(msg: String): Unit = {
Expand Down Expand Up @@ -199,13 +214,29 @@ class MasterRunner(theArgs: Array[String], theRemoteArgs: Array[String], testCla
None
}

def serializeTask(task: Task, serializer: (TaskDef) => String): String =
serializer(task.taskDef())
def serializeTask(task: Task, serializer: (TaskDef) => String): String = {
val knownSuitesInfoSerialized = task match {
case taskRunner: TaskRunner =>
TaskRunner.serializeKnownSuites(taskRunner)
case _ => ""
}
knownSuitesInfoSerialized + serializer(task.taskDef())
}

def deserializeTask(task: String, deserializer: (String) => TaskDef): Task = {
val taskDef = deserializer(task)
new TaskRunner(taskDef, testClassLoader, tracker, tagsToInclude, tagsToExclude, taskDef.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
presentReminderWithShortStackTraces, presentReminderWithFullStackTraces, presentReminderWithoutCanceledTests, presentFilePathname, presentJson, Some(send))
// ignore serialized suite info and use saved `knownSuites` instead if we're on the master thread
val taskIgnoreSuites = TaskRunner.stripKnownSuites(task)
val taskDef = deserializer(taskIgnoreSuites)
val knownSuites = this.knownSuites
val key = taskDef.fullyQualifiedName() -> taskDef.selectors().toList
knownSuites.get(key) match {
case Some(knownTask) =>
knownTask
case None =>
val knownRunningSuites = knownSuites.map(_._2.runningSuite).toList
new TaskRunner(taskDef, () => knownRunningSuites, true, testClassLoader, tracker, tagsToInclude, tagsToExclude, taskDef.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
presentReminderWithShortStackTraces, presentReminderWithFullStackTraces, presentReminderWithoutCanceledTests, presentFilePathname, presentJson, Some(send))
}
}

}
35 changes: 26 additions & 9 deletions js/core/src/main/scala/org/scalatest/tools/SlaveRunner.scala
Expand Up @@ -15,7 +15,7 @@
*/
package org.scalatest.tools

import org.scalatest.{Resources, Tracker}
import org.scalatest.{Resources, RunningSuite, Tracker}
import org.scalatest.events.Summary
import sbt.testing.{Framework => BaseFramework, Event => SbtEvent, Status => SbtStatus, _}
import ArgsParser._
Expand Down Expand Up @@ -132,23 +132,40 @@ class SlaveRunner(theArgs: Array[String], theRemoteArgs: Array[String], testClas
paths.exists(path => td.fullyQualifiedName.startsWith(path) && td.fullyQualifiedName.substring(path.length).lastIndexOf('.') <= 0)
}

def createTask(t: TaskDef): Task =
new TaskRunner(t, testClassLoader, tracker, tagsToInclude, tagsToExclude, t.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
def createTask(allRunningSuites: () => List[RunningSuite], t: TaskDef): TaskRunner =
new TaskRunner(t, allRunningSuites, false, testClassLoader, tracker, tagsToInclude, tagsToExclude, t.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
presentReminderWithShortStackTraces, presentReminderWithFullStackTraces, presentReminderWithoutCanceledTests, presentFilePathname, presentJson, Some(notifyServer))

(if (wildcard.isEmpty && membersOnly.isEmpty) taskDefs else (filterWildcard(wildcard, taskDefs) ++ filterMembersOnly(membersOnly, taskDefs)).distinct).map(createTask)
val chosenTaskDefs = if (wildcard.isEmpty && membersOnly.isEmpty) taskDefs else (filterWildcard(wildcard, taskDefs) ++ filterMembersOnly(membersOnly, taskDefs)).distinct

lazy val taskRunners: Array[TaskRunner] = {
val allRunningSuites = () => taskRunners.map(_.runningSuite).toList
chosenTaskDefs.map(createTask(allRunningSuites, _))
}

taskRunners.toArray[Task]
}

def receiveMessage(msg: String): Option[String] =
None

def serializeTask(task: Task, serializer: (TaskDef) => String): String =
serializer(task.taskDef())
def serializeTask(task: Task, serializer: (TaskDef) => String): String = {
val knownSuitesInfoSerialized = task match {
case taskRunner: TaskRunner =>
TaskRunner.serializeKnownSuites(taskRunner)
case _ => ""
}
knownSuitesInfoSerialized + serializer(task.taskDef())
}

def deserializeTask(task: String, deserializer: (String) => TaskDef): Task = {
val taskDef = deserializer(task)
new TaskRunner(taskDef, testClassLoader, tracker, tagsToInclude, tagsToExclude, taskDef.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
// on Scala.js worker thread has no access to global mutable state, so the best
// we can do is deserialize classNames serialized on the MasterRunner
val (runningSuites, taskIgnoreSuites) = TaskRunner.deserializeKnownSuites(task)
val taskDef = deserializer(taskIgnoreSuites)

new TaskRunner(taskDef, () => runningSuites, false, testClassLoader, tracker, tagsToInclude, tagsToExclude, taskDef.selectors ++ autoSelectors, false, presentAllDurations, presentInColor, presentShortStackTraces, presentFullStackTraces, presentUnformatted, presentReminder,
presentReminderWithShortStackTraces, presentReminderWithFullStackTraces, presentReminderWithoutCanceledTests, presentFilePathname, presentJson, Some(notifyServer))
}

}
}
@@ -0,0 +1,11 @@
package org.scalatest.tools

import org.scalatest.{RunningSuite, Suite}
import scala.scalajs.reflect.Reflect

private[tools] object SuiteInstantiationHelper {
def createRunningSuite(className: String, isMaster: Boolean): RunningSuite = {
lazy val suite: Suite = Reflect.lookupInstantiatableClass(className).getOrElse(throw new RuntimeException("Cannot load suite class: " + className)).newInstance().asInstanceOf[Suite]
RunningSuite(className, () => suite, isMaster)
}
}
39 changes: 36 additions & 3 deletions js/core/src/main/scala/org/scalatest/tools/TaskRunner.scala
Expand Up @@ -26,7 +26,6 @@ import org.scalatest.events.{TestFailed,
SeeStackDepthException}
import org.scalatest.tools.StringReporter._
import sbt.testing._
import scala.scalajs.reflect.Reflect
import org.scalatest._
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent.Promise
Expand All @@ -38,6 +37,8 @@ import scala.compat.Platform
import scala.concurrent.duration.Duration

final class TaskRunner(task: TaskDef,
val allRunningSuites: () => List[RunningSuite],
isMaster: Boolean,
cl: ClassLoader,
tracker: Tracker,
tagsToInclude: Set[String],
Expand All @@ -56,6 +57,8 @@ final class TaskRunner(task: TaskDef,
presentFilePathname: Boolean,
presentJson: Boolean,
notifyServer: Option[String => Unit]) extends Task {
val runningSuite: RunningSuite = SuiteInstantiationHelper.createRunningSuite(task.fullyQualifiedName(), isMaster)

def tags(): Array[String] = Array.empty
def taskDef(): TaskDef = task

Expand All @@ -76,7 +79,7 @@ final class TaskRunner(task: TaskDef,

def executionFuture(eventHandler: EventHandler, loggers: Array[Logger]): Future[Unit] = {
val suiteStartTime = Platform.currentTime
val suite = Reflect.lookupInstantiatableClass(task.fullyQualifiedName).getOrElse(throw new RuntimeException("Cannot load suite class: " + task.fullyQualifiedName)).newInstance().asInstanceOf[Suite]
val suite = runningSuite.lazyHandle.apply()
val sbtLogInfoReporter = new SbtLogInfoReporter(
loggers,
presentAllDurations,
Expand Down Expand Up @@ -143,7 +146,7 @@ final class TaskRunner(task: TaskDef,
if (!suite.isInstanceOf[DistributedTestRunnerSuite])
reporter(SuiteStarting(tracker.nextOrdinal(), suite.suiteName, suite.suiteId, Some(suiteClassName), formatter, Some(TopOfClass(suiteClassName))))

val args = Args(reporter, Stopper.default, filter, ConfigMap.empty, None, tracker)
val args = Args(reporter, Stopper.default, filter, ConfigMap.empty, None, tracker, allRunningSuites())

val future: Future[Unit] =
try {
Expand Down Expand Up @@ -248,3 +251,33 @@ final class TaskRunner(task: TaskDef,
def dispose() = ()
}
}

object TaskRunner {
def serializeKnownSuites(taskRunner: TaskRunner): String = {
KnownSuitesPreamble +
taskRunner.allRunningSuites().map(_.className).mkString(KnownSuitesSeparator) +
KnownSuitesEnd
}

def deserializeKnownSuites(task: String): (List[RunningSuite], String) = {
val afterPreamble = task.stripPrefix(KnownSuitesPreamble)
if (afterPreamble != task) {
val endingPosition = afterPreamble.indexOf(KnownSuitesEnd)
val (suitesStr, restWithEnding) = afterPreamble.splitAt(endingPosition)
val classNames = suitesStr.split(KnownSuitesSeparator)
val runningSuites = classNames.map(SuiteInstantiationHelper.createRunningSuite(_, false)).toList
val rest = restWithEnding.stripPrefix(KnownSuitesEnd)

(runningSuites, rest)
} else (List.empty, task)
}

def stripKnownSuites(task: String): String = {
val endingPosition = task.indexOf(KnownSuitesEnd)
task.drop(endingPosition).stripPrefix(KnownSuitesEnd)
}

private val KnownSuitesPreamble = "{org.scalatest.tools.TaskRunner.allRunningSuites}[[["
private val KnownSuitesSeparator = ";;;"
private val KnownSuitesEnd = "]]]{org.scalatest.tools.TaskRunner.allRunningSuites}"
}
12 changes: 6 additions & 6 deletions jvm/common-test/src/main/scala/org/scalatest/SharedHelpers.scala
Expand Up @@ -263,7 +263,7 @@ object SharedHelpers extends Assertions with LineNumberHelper {

def getIndexesForTestInformerEventOrderTests(suite: Suite, testName: String, infoMsg: String): (Int, Int) = {
val myRep = new EventRecordingReporter
suite.run(None, Args(myRep))
suite.run(None, Args(myRep, runningSuites = List.empty))

val indexedList = myRep.eventsReceived.zipWithIndex

Expand Down Expand Up @@ -293,7 +293,7 @@ object SharedHelpers extends Assertions with LineNumberHelper {
def getIndexesForInformerEventOrderTests(suite: Suite, testName: String, infoMsg: String): (Int, Int, Int) = {

val myRep = new EventRecordingReporter
suite.run(None, Args(myRep))
suite.run(None, Args(myRep, runningSuites = List.empty))

val indexedList = myRep.eventsReceived.zipWithIndex

Expand Down Expand Up @@ -326,7 +326,7 @@ object SharedHelpers extends Assertions with LineNumberHelper {
def getIndentedTextFromInfoProvided(suite: Suite): IndentedText = {

val myRep = new EventRecordingReporter
suite.run(None, Args(myRep))
suite.run(None, Args(myRep, runningSuites = List.empty))

val infoProvidedOption = myRep.eventsReceived.find(_.isInstanceOf[InfoProvided])

Expand All @@ -342,7 +342,7 @@ object SharedHelpers extends Assertions with LineNumberHelper {

def getIndentedTextFromTestInfoProvided(suite: Suite): IndentedText = {
val myRep = new EventRecordingReporter
suite.run(None, Args(myRep))
suite.run(None, Args(myRep, runningSuites = List.empty))
val recordedEvents: Seq[Event] = myRep.eventsReceived.find { e =>
e match {
case testSucceeded: TestSucceeded =>
Expand Down Expand Up @@ -385,15 +385,15 @@ object SharedHelpers extends Assertions with LineNumberHelper {

def ensureTestFailedEventReceived(suite: Suite, testName: String): Unit = {
val reporter = new EventRecordingReporter
suite.run(None, Args(reporter))
suite.run(None, Args(reporter, runningSuites = List.empty))
val testFailedEvent = reporter.eventsReceived.find(_.isInstanceOf[TestFailed])
assert(testFailedEvent.isDefined)
assert(testFailedEvent.get.asInstanceOf[TestFailed].testName === testName)
}

def ensureTestFailedEventReceivedWithCorrectMessage(suite: Suite, testName: String, expectedMessage: String): Unit = {
val reporter = new EventRecordingReporter
suite.run(None, Args(reporter))
suite.run(None, Args(reporter, runningSuites = List.empty))
val testFailedEvent = reporter.eventsReceived.find(_.isInstanceOf[TestFailed])
assert(testFailedEvent.isDefined)
assert(testFailedEvent.get.asInstanceOf[TestFailed].testName == testName)
Expand Down
Expand Up @@ -3,7 +3,7 @@ package org.scalatest
import org.scalatest.SharedHelpers.SilentReporter
import java.util.concurrent.ExecutorService

class TestConcurrentDistributor(execService: ExecutorService) extends tools.ConcurrentDistributor(Args(reporter = SilentReporter), execService) {
class TestConcurrentDistributor(execService: ExecutorService) extends tools.ConcurrentDistributor(Args(reporter = SilentReporter, runningSuites = List.empty), execService) {
override def apply(suite: Suite, tracker: Tracker): Unit = {
throw new UnsupportedOperationException("Please use apply with args.")
}
Expand Down
2 changes: 2 additions & 0 deletions jvm/core/src/main/scala/org/scalatest/Args.scala
Expand Up @@ -51,6 +51,7 @@ import org.scalatest.time.{Seconds, Span}
* for the parallel-executed tests of one suite back into sequential order on the fly, with a timeout in case a test takes too long to complete
* @param distributedSuiteSorter an optional <a href="DistributedSuiteSorter.html"><code>DistributedSuiteSorter</code></a> used by <code>ParallelTestExecution</code> to ensure the events
* for the parallel-executed suites are sorted back into sequential order, with a timeout in case a suite takes to long to complete, even when tests are executed in parallel
* @param runningSuites information about all the suites running in the current test run. The <code>List</code> may be empty if the current suite is the only suite in this run.
* @throws NullArgumentException if any passed parameter is <code>null</code>.
*
*/
Expand All @@ -61,6 +62,7 @@ case class Args(
configMap: ConfigMap = ConfigMap.empty,
distributor: Option[Distributor] = None,
tracker: Tracker = Tracker.default,
runningSuites: List[RunningSuite] = List.empty,
runTestInNewInstance: Boolean = false,
distributedTestSorter: Option[DistributedTestSorter] = None,
distributedSuiteSorter: Option[DistributedSuiteSorter] = None
Expand Down
27 changes: 27 additions & 0 deletions jvm/core/src/main/scala/org/scalatest/RunningSuite.scala
@@ -0,0 +1,27 @@
package org.scalatest

import org.scalactic.Requirements.requireNonNull

/**
* Contains information and a handle to a discovered <a href="Suite.html"><code>Suite</code></a> that will be ran or is already running in the current test run.
*
* @param className the name of the class of the suite
* @param lazyHandle the lazy handle to a (usually singleton) instance of the suite.
* If the suite hasn't been instantiated yet, calling this function will instantiate it.
* @param isSingleton indicates whether calling <code>lazyHandle</code> will return a singleton instance of the Suite.
* Always <code>true</code> on the JVM.
* However on Scala.js and Scala Native, when tests run using a worker (<code>org.scalatest.tools.SlaveRunner</code>),
* singleton-ness cannot be guaranteed anymore.
* As of the time of writing, tests on Scala Native nearly always run in separate worker processes,
* making singleton guarantees impossible.
* If this is <code>false</code>, running <code>lazyHandle</code> will duplicate the Suite.
*
* @throws NullArgumentException if any passed parameter is <code>null</code>.
*/
case class RunningSuite(
className: String,
lazyHandle: () => Suite,
isSingleton: Boolean
) {
requireNonNull(className, lazyHandle, isSingleton)
}
Expand Up @@ -64,7 +64,7 @@ trait StepwiseNestedSuiteExecution extends SuiteMixin { thisSuite: Suite =>

try {
// Same thread, so OK to send same tracker
val status = nestedSuite.run(None, Args(report, stopper, filter, configMap, distributor, tracker))
val status = nestedSuite.run(None, Args(report, stopper, filter, configMap, distributor, tracker, runningSuites))

val rawString = Resources.suiteCompletedNormally
val formatter = formatterForSuiteCompleted(nestedSuite)
Expand Down
7 changes: 3 additions & 4 deletions jvm/core/src/main/scala/org/scalatest/Suite.scala
Expand Up @@ -820,7 +820,8 @@ trait Suite extends Assertions with Serializable { thisSuite =>
filter,
configMap,
None,
tracker)
tracker,
List(RunningSuite(suiteClassName, () => this, true)))
)
status.waitUntilCompleted()
val suiteCompletedFormatter = formatterForSuiteCompleted(thisSuite)
Expand Down Expand Up @@ -1174,7 +1175,7 @@ trait Suite extends Assertions with Serializable { thisSuite =>

try {
// Same thread, so OK to send same tracker
val status = nestedSuite.run(None, Args(report, stopper, filter, configMap, distributor, tracker))
val status = nestedSuite.run(None, Args(report, stopper, filter, configMap, distributor, tracker, runningSuites))

val rawString = Resources.suiteCompletedNormally
val formatter = formatterForSuiteCompleted(nestedSuite)
Expand Down Expand Up @@ -2126,5 +2127,3 @@ used for test events like succeeded/failed, etc.
}
// SKIP-SCALATESTJS-END
}