blob: ce209a68f2d69eb03c5ef11efdfbb5f7c866686b [file] [log] [blame]
/*
* Copyright 2015-2016 IBM Corporation
*
* 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 common
import scala.collection.mutable.ListBuffer
import scala.util.Failure
import scala.util.Try
import scala.concurrent.duration.Duration
import scala.concurrent.duration.DurationInt
import scala.language.postfixOps
import org.scalatest.Matchers
import TestUtils._
import spray.json._
import java.time.Instant
/**
* Test fixture to ease cleaning of whisk entities created during testing.
*
* The fixture records the entities created during a test and when the test
* completed, will delete them all.
*/
trait WskTestHelpers extends Matchers {
type Assets = ListBuffer[(DeleteFromCollection, String, Boolean)]
/**
* Helper to register an entity to delete once a test completes.
* The helper sanitizes (deletes) a previous instance of the entity if it exists
* in given collection.
*
*/
class AssetCleaner(assetsToDeleteAfterTest: Assets, wskprops: WskProps) {
def withCleaner[T <: DeleteFromCollection](cli: T, name: String, confirmDelete: Boolean = true)(
cmd: (T, String) => RunResult): RunResult = {
// sanitize (delete) if asset exists
cli match {
case _: WskPackage =>
val rr = cli.sanitize(name)(wskprops)
rr.exitCode match {
case CONFLICT =>
// retry sanitization on a package since there may be a list (view)
// operation that requires a retry for eventual consistency
whisk.utils.retry({
cli.sanitize(name)(wskprops)
}, 5, Some(1 second))
case _ => rr
}
case _ => cli.sanitize(name)(wskprops)
}
assetsToDeleteAfterTest += ((cli, name, confirmDelete))
cmd(cli, name)
}
}
/**
* Creates a test closure which records all entities created inside the test into a
* list that is iterated at the end of the test so that these entities are deleted
* (from most recently created to oldest).
*/
def withAssetCleaner(wskprops: WskProps)(test: (WskProps, AssetCleaner) => Any) = {
// create new asset list to track what must be deleted after test completes
val assetsToDeleteAfterTest = new Assets()
try {
test(wskprops, new AssetCleaner(assetsToDeleteAfterTest, wskprops))
} catch {
case t: Throwable =>
// log the exception that occurred in the test and rethrow it
println(s"Exception occurred during test execution: $t")
throw t
} finally {
// delete assets in reverse order so that was created last is deleted first
val deletedAll = assetsToDeleteAfterTest.reverse map {
case ((cli, n, delete)) => n -> Try {
if (delete) {
cli.delete(n)(wskprops)
} else {
cli.sanitize(n)(wskprops)
}
}
} forall {
case (n, Failure(t)) =>
println(s"ERROR: deleting asset failed for $n: $t")
false
case _ =>
true
}
assert(deletedAll, "some assets were not deleted")
}
}
/**
* An arbitrary response of a whisk action. Includes the result as a JsObject as the
* structure of "result" is not defined.
*/
case class CliActivationResponse(result: Option[JsObject], status: String, success: Boolean)
object CliActivationResponse extends DefaultJsonProtocol {
implicit val serdes = jsonFormat3(CliActivationResponse.apply)
}
/**
* Activation record as it is returned by the CLI.
*/
case class CliActivation(
activationId: String,
logs: Option[List[String]],
response: CliActivationResponse,
start: Long,
end: Long,
duration: Long,
cause: Option[String],
annotations: Option[List[JsObject]]) {
def getAnnotationValue(key: String): Option[JsValue] = {
Try {
val annotation = annotations.get.filter(x => x.getFields("key")(0) == JsString(key))
assert(annotation.size == 1) // only one annotation with this value
val value = annotation(0).getFields("value")
assert(value.size == 1)
value(0)
} toOption
}
}
object CliActivation extends DefaultJsonProtocol {
implicit val serdes = jsonFormat8(CliActivation.apply)
}
/**
* Extracts an activation id from a wsk command producing a RunResult with such an id.
* If id is found, polls activations until one matching id is found. If found, pass
* the activation to the post processor which then check for expected values.
*/
def withActivation(
wsk: WskActivation,
run: RunResult,
initialWait: Duration = 1 second,
pollPeriod: Duration = 1 second,
totalWait: Duration = 30 seconds)(
check: CliActivation => Unit)(
implicit wskprops: WskProps): Unit = {
val activationId = wsk.extractActivationId(run)
withClue(s"did not find an activation id in '$run'") {
activationId shouldBe a[Some[_]]
}
withActivation(wsk, activationId.get, initialWait, pollPeriod, totalWait)(check)
}
/**
* Polls activations until one matching id is found. If found, pass
* the activation to the post processor which then check for expected values.
*/
def withActivation(
wsk: WskActivation,
activationId: String,
initialWait: Duration,
pollPeriod: Duration,
totalWait: Duration)(
check: CliActivation => Unit)(
implicit wskprops: WskProps): Unit = {
val id = activationId
val activation = wsk.waitForActivation(id, initialWait, pollPeriod, totalWait)
if (activation.isLeft) {
assert(false, s"error waiting for activation $id: ${activation.left.get}")
} else try {
check(activation.right.get.convertTo[CliActivation])
} catch {
case error: Throwable =>
println(s"check failed for activation $id: ${activation.right.get}")
throw error
}
}
/**
* Polls until it finds {@code N} activationIds from an entity. Asserts the count
* of the activationIds actually equal {@code N}. Takes a {@code since} parameter
* defining the oldest activationId to consider valid.
*/
def withActivationsFromEntity(
wsk: WskActivation,
entity: String,
N: Int = 1,
since: Option[Instant] = None,
pollPeriod: Duration = 1 second,
totalWait: Duration = 30 seconds)(
check: Seq[CliActivation] => Unit)(
implicit wskprops: WskProps): Unit = {
val activationIds = wsk.pollFor(N, Some(entity), since = since, retries = (totalWait / pollPeriod).toInt, pollPeriod = pollPeriod)
withClue(s"expecting $N activations matching '$entity' name since $since but found ${activationIds.mkString(",")} instead") {
activationIds.length shouldBe N
}
val parsed = activationIds.map { id =>
wsk.parseJsonString(wsk.get(id).stdout).convertTo[CliActivation]
}
try {
check(parsed)
} catch {
case error: Throwable =>
println(s"check failed for activations $activationIds: ${parsed}")
throw error
}
}
/**
* In the case that test throws an exception, print stderr and stdout
* from the provided RunResult.
*/
def withPrintOnFailure(runResult: RunResult)(test: () => Unit) {
try {
test()
} catch {
case error: Throwable =>
println(s"[stderr] ${runResult.stderr}")
println(s"[stdout] ${runResult.stdout}")
throw error
}
}
def removeCLIHeader(response: String): String = response.substring(response.indexOf("\n"))
def getJSONFromCLIResponse(response: String): JsObject = removeCLIHeader(response).parseJson.asJsObject
}