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

wvlet.airspec.runner.AirSpecTaskRunner.scala Maven / Gradle / Ivy

There is a newer version: 20.12.0
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package wvlet.airspec.runner

import sbt.testing._
import wvlet.airframe.{Design, Session}
import wvlet.airspec.runner.AirSpecSbtRunner.AirSpecConfig
import wvlet.airspec.spi.{AirSpecContext, AirSpecException}
import wvlet.log.LogSupport

import scala.util.{Failure, Success, Try}

/**
  * AirSpecTaskRunner will execute a task.
  *
  * For each test spec (AirSpec instance), it will create a global airframe session,
  * which can be configured with configure(Design).
  *
  * For each test method in the AirSpec instance, it will create a child session so that
  * users can manage test-method local instances, which will be discarded after the completion of the test method.
  */
private[airspec] class AirSpecTaskRunner(
    taskDef: TaskDef,
    config: AirSpecConfig,
    taskLogger: AirSpecLogger,
    eventHandler: EventHandler,
    classLoader: ClassLoader
) extends LogSupport {
  import wvlet.airspec._

  def runTask: Unit = {
    val testClassName = taskDef.fullyQualifiedName()
    val leafName      = AirSpecSpi.leafClassName(AirSpecSpi.decodeClassName(testClassName))

    val startTimeNanos = System.nanoTime()
    try {
      // Start a background log level scanner thread. If a thread is already running, reuse it.
      compat.withLogScanner {
        trace(s"Processing task: ${taskDef}")
        // Getting an instance of AirSpec
        val testObj = taskDef.fingerprint() match {
          // In Scala.js we cannot use pattern match for objects like AirSpecObjectFingerPrint, so using isModule here.
          case c: SubclassFingerprint if c.isModule =>
            compat.findCompanionObjectOf(testClassName, classLoader)
          case _ =>
            compat.newInstanceOf(testClassName, classLoader)
        }

        testObj match {
          case Some(spec: AirSpecSpi) =>
            run(parentContext = None, spec, spec.testDefinitions)
          case _ =>
            taskLogger.logSpecName(leafName, indentLevel = 0)
            throw new IllegalStateException(
              s"${testClassName} needs to be a class or object extending AirSpec: ${testObj.getClass}"
            )
        }
      }
    } catch {
      case e: Throwable =>
        taskLogger.logSpecName(leafName, indentLevel = 0)
        val cause  = compat.findCause(e)
        val status = AirSpecException.classifyException(cause)
        // Unknown error
        val event =
          AirSpecEvent(taskDef, "", status, new OptionalThrowable(cause), System.nanoTime() - startTimeNanos)
        taskLogger.logEvent(event)
        eventHandler.handle(event)
    }
  }

  private[airspec] def run(parentContext: Option[AirSpecContext], spec: AirSpecSpi, testDefs: Seq[AirSpecDef]): Unit = {
    if (testDefs.isEmpty && Compat.isScalaJs) {
      val name = specName(parentContext, spec)
      warn(
        s"No test definition is found in ${name}. In Scala.js make sure calling scalaJsSupport inside test classes to register" +
          s" public methods as test cases. Alternatively, you can use test(...) function, which works without calling scalaJsSupport."
      )
    }

    val selectedMethods =
      config.pattern match {
        case Some(regex) =>
          // Find matching methods
          testDefs.filter { m =>
            // Concatenate (parent class name)? + class name + method name for handy search
            val fullName = s"${specName(parentContext, spec)}.${m.name}"
            regex.findFirstIn(fullName).isDefined
          }
        case None =>
          testDefs
      }

    if (selectedMethods.nonEmpty) {
      runSpec(parentContext, spec, selectedMethods)
    }
  }

  private def specName(parentContext: Option[AirSpecContext], spec: AirSpecSpi): String = {
    val parentName = parentContext.map(x => s"${x.specName}.").getOrElse("")
    s"${parentName}${spec.leafSpecName}"
  }

  private def runSpec(
      parentContext: Option[AirSpecContext],
      spec: AirSpecSpi,
      targetTestDefs: Seq[AirSpecDef]
  ): Unit = {
    val indentLevel = parentContext.map(_.indentLevel + 1).getOrElse(0)
    taskLogger.logSpecName(spec.leafSpecName, indentLevel = indentLevel)

    try {
      // Start the spec
      spec.callBeforeAll

      // Configure the global spec design
      var d = Design.newDesign.noLifeCycleLogging
      d = d + spec.callDesign

      // Create a global Airframe session
      val globalSession =
        parentContext
          .map(_.currentSession.newChildSession(d))
          .getOrElse { d.newSessionBuilder.noShutdownHook.build } // Do not register JVM shutdown hooks

      val localDesign = spec.callLocalDesign
      globalSession.start {
        for (m <- targetTestDefs) {
          runSingle(parentContext, globalSession, spec, m, isLocal = false, design = localDesign)
        }
      }
    } finally {
      spec.callAfterAll
    }
  }

  private var displayedContext = Set.empty[String]

  private[airspec] def runSingle(
      parentContext: Option[AirSpecContext],
      globalSession: Session,
      spec: AirSpecSpi,
      m: AirSpecDef,
      isLocal: Boolean,
      design: Design
  ): Unit = {

    val ctxName = parentContext.map(ctx => s"${ctx.fullSpecName}.${ctx.testName}").getOrElse("N/A")

    val indentLevel = parentContext.map(_.indentLevel + 1).getOrElse(0)

    // Show the inner test name
    if (isLocal) {
      parentContext.map { ctx =>
        synchronized {
          if (!displayedContext.contains(ctxName)) {
            taskLogger.logTestName(ctx.testName, indentLevel = (indentLevel - 1).max(0))
            displayedContext += ctxName
          }
        }
      }
    }

    spec.callBefore
    // Configure the test-local design
    val childDesign = design + m.design

    val startTimeNanos = System.nanoTime()

    var hadChildTask = false

    // Create a test-method local child session
    val result = globalSession.withChildSession(childDesign) { childSession =>
      val context =
        new AirSpecContextImpl(
          this,
          parentContext = parentContext,
          currentSpec = spec,
          testName = m.name,
          currentSession = childSession
        )
      spec.pushContext(context)
      // Wrap the execution with Try[_] to report the test result to the event handler
      Try {
        try {
          m.run(context, childSession)
        } finally {
          spec.callAfter
          spec.popContext

          // If the test method had any child task, update the flag
          hadChildTask |= context.hasChildTask
        }
      }
    }
    // Report the test result
    val durationNanos = System.nanoTime() - startTimeNanos

    val (status, throwableOpt) = result match {
      case Success(x) =>
        (Status.Success, new OptionalThrowable())
      case Failure(ex) =>
        val status = AirSpecException.classifyException(ex)
        (status, new OptionalThrowable(compat.findCause(ex)))
    }

    val e = AirSpecEvent(taskDef, m.name, status, throwableOpt, durationNanos)
    taskLogger.logEvent(e, indentLevel = indentLevel, showTestName = !hadChildTask)
    eventHandler.handle(e)
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy