Skip to content

Commit

Permalink
NODE-2588 Qase.io integration (#3845)
Browse files Browse the repository at this point in the history
  • Loading branch information
DrBlast authored Jul 6, 2023
1 parent b0fad2a commit 25b60c3
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 33 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/check-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ jobs:
key: coursier-cache
- name: Check PR
run: sbt --mem 6144 --batch checkPR
env:
QASE_ENABLE: true
QASE_RUN_NAME: checkPR
QASE_RUN_ID: 1
QASE_PROJECT_CODE: PR
QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }}
CHECKPR_RUN_ID: ${{ github.run_id }}
- uses: dorny/paths-filter@v2
id: filter
with:
Expand Down
53 changes: 32 additions & 21 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ lazy val `lang-testkit` = project
.dependsOn(`lang-jvm`)
.in(file("lang/testkit"))
.settings(
libraryDependencies ++= Dependencies.test.map(_.withConfigurations(Some("compile")))
libraryDependencies ++= Dependencies.test.map(_.withConfigurations(Some("compile"))) ++ Dependencies.qaseReportDeps
)

lazy val `lang-tests` = project
.in(file("lang/tests"))
.dependsOn(`lang-testkit`)
.dependsOn(`lang-testkit` % "test;test->test")

lazy val `lang-tests-js` = project
.in(file("lang/tests-js"))
Expand All @@ -62,7 +62,7 @@ lazy val `lang-tests-js` = project
testFrameworks += new TestFramework("utest.runner.Framework")
)

lazy val node = project.dependsOn(`lang-jvm`, `lang-testkit` % "test")
lazy val node = project.dependsOn(`lang-jvm`, `lang-testkit` % "test;test->test")

lazy val `grpc-server` = project.dependsOn(node % "compile;test->test;runtime->provided")
lazy val `node-it` = project.dependsOn(node % "compile;test->test", `lang-testkit`, `repl-jvm`, `grpc-server`)
Expand Down Expand Up @@ -92,7 +92,7 @@ lazy val repl = crossProject(JSPlatform, JVMPlatform)
)

lazy val `repl-jvm` = repl.jvm
.dependsOn(`lang-jvm`, `lang-testkit` % "test")
.dependsOn(`lang-jvm`, `lang-testkit` % "test;test->test")
.settings(
libraryDependencies ++= Dependencies.circe.value ++ Seq(
"org.scala-js" %% "scalajs-stubs" % "1.1.0" % Provided,
Expand Down Expand Up @@ -154,7 +154,7 @@ inScope(Global)(
* F - show full stack traces
* u - select the JUnit XML reporter with output directory
*/
testOptions += Tests.Argument("-oIDOF", "-u", "target/test-reports"),
testOptions += Tests.Argument("-oIDOF", "-u", "target/test-reports", "-C", "com.wavesplatform.report.QaseReporter"),
testOptions += Tests.Setup(_ => sys.props("sbt-testing") = "true"),
network := Network.default(),
instrumentation := false,
Expand Down Expand Up @@ -188,23 +188,34 @@ buildTarballsForDocker := {
}

lazy val checkPRRaw = taskKey[Unit]("Build a project and run unit tests")
checkPRRaw := Def
.sequential(
`waves-node` / clean,
Def.task {
(`lang-tests` / Test / test).value
(`repl-jvm` / Test / test).value
(`lang-js` / Compile / fastOptJS).value
(`lang-tests-js` / Test / test).value
(`grpc-server` / Test / test).value
(node / Test / test).value
(`repl-js` / Compile / fastOptJS).value
(`node-it` / Test / compile).value
(benchmark / Test / compile).value
(`node-generator` / Compile / compile).value
checkPRRaw := Def.taskDyn {
val res = Def
.sequential(
`waves-node` / clean,
Def.task {
(`lang-tests` / Test / test).value
(`repl-jvm` / Test / test).value
(`lang-js` / Compile / fastOptJS).value
(`lang-tests-js` / Test / test).value
(`grpc-server` / Test / test).value
(node / Test / test).value
(`repl-js` / Compile / fastOptJS).value
(`node-it` / Test / compile).value
(benchmark / Test / compile).value
(`node-generator` / Compile / compile).value
}
)
.result
.value

Def.task {
(`lang-testkit` / Test / runMain).toTask(" com.wavesplatform.report.QaseRunCompleter").value
res match {
case Inc(inc: Incomplete) => throw inc
case Value(v) => v
}
)
.value
}
}.value

def checkPR: Command = Command.command("checkPR") { state =>
val newState = Project
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.wavesplatform.report

import com.wavesplatform.report.QaseReporter.{CaseIdPattern, QaseProjects, TestResult}
import io.qase.api.QaseClient
import io.qase.api.utils.IntegrationUtils
import io.qase.client.model.ResultCreate
import org.scalatest.Reporter
import org.scalatest.events.*
import play.api.libs.json.{Format, Json}

import java.io.FileWriter
import java.util.concurrent.ConcurrentHashMap
import scala.jdk.CollectionConverters.*
import scala.util.matching.Regex

class QaseReporter extends Reporter {

private val results = initResults()

override def apply(event: Event): Unit = {
event match {
case ts: TestSucceeded =>
extractCaseIds(ts.testName).foreach { case (projectCode, caseId) =>
saveTestCaseResults(ResultCreate.StatusEnum.PASSED, ts.testName, projectCode, caseId, None, None, ts.duration)
}
case tf: TestFailed =>
extractCaseIds(tf.testName).foreach { case (projectCode, caseId) =>
saveTestCaseResults(ResultCreate.StatusEnum.FAILED, tf.testName, projectCode, caseId, tf.throwable, Some(tf.message), tf.duration)
}
case _: RunAborted | _: RunStopped | _: RunCompleted =>
saveRunResults()
case _ => ()
}
}

private def extractCaseIds(testName: String): Seq[(String, Long)] =
CaseIdPattern.findAllMatchIn(testName).map(m => m.group(1) -> m.group(2).toLong).toSeq

private def saveTestCaseResults(
status: ResultCreate.StatusEnum,
testName: String,
projectCode: String,
caseId: Long,
throwable: Option[Throwable],
msgOpt: Option[String],
duration: Option[Long]
): Unit =
if (QaseClient.isEnabled) {
val errMsg = msgOpt.map(msg => s"\n\n**Error**\n$msg").getOrElse("")
val comment = s"$testName$errMsg"
val stacktrace = throwable.map(IntegrationUtils.getStacktrace)
val timeMs = duration.getOrElse(0L)

results.computeIfPresent(projectCode, (_, results) => TestResult(status.toString, comment, stacktrace, caseId, timeMs) +: results)
}

private def saveRunResults(): Unit =
if (QaseClient.isEnabled) {
results.asScala.foreach { case (projectCode, results) =>
if (results.nonEmpty) {
val writer = new FileWriter(s"./$projectCode-${System.currentTimeMillis()}")
writer.write(Json.toJson(results).toString)
writer.close()
}
}
}

private def initResults() = {
val results = new ConcurrentHashMap[String, List[TestResult]]()
QaseProjects.foreach(projectCode => results.put(projectCode, List.empty))
results
}
}

object QaseReporter {
val RunIdKeyPrefix = "QASE_RUN_ID_"
val CheckPRRunIdKey = "CHECKPR_RUN_ID"
val QaseProjects = Seq("NODE", "RIDE", "BU", "SAPI")

private val patternStr = s"""(${QaseProjects.mkString("|")})-([0-9]+)"""
val CaseIdPattern: Regex = patternStr.r

case class TestResult(status: String, comment: String, stackTrace: Option[String], caseId: Long, timeMs: Long)
object TestResult {
implicit val format: Format[TestResult] = Json.format
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.wavesplatform.report

import com.wavesplatform.report.QaseReporter.{CheckPRRunIdKey, QaseProjects, TestResult}
import io.qase.api.QaseClient
import io.qase.api.config.QaseConfig.{PROJECT_CODE_KEY, RUN_ID_KEY}
import io.qase.api.services.impl.ReportersResultOperationsImpl
import io.qase.client.ApiClient
import io.qase.client.api.{CasesApi, ResultsApi, RunsApi}
import io.qase.client.model.{GetCasesFiltersParameter, GetRunsFiltersParameter, ResultCreate, RunCreate}
import play.api.libs.json.Json

import java.io.File
import java.nio.file.Files
import scala.annotation.tailrec
import scala.io.Source
import scala.util.Using

object QaseRunCompleter extends App {
if (QaseClient.getConfig.isEnabled) {

val apiClient: ApiClient = QaseClient.getApiClient
val runsApi = new RunsApi(apiClient)
val resultsApi = new ResultsApi(apiClient)
val casesApi = new CasesApi(apiClient)

QaseProjects.foreach { projectCode =>
waitForActiveRunComplete(runsApi)

val dir = new File("./")
val resultFiles = dir.listFiles
.filter(_.isFile)
.filter(_.getName.startsWith(projectCode))
.toSeq

Using.resource(resultFiles) { resultFiles =>
val hasCases = casesApi.getCases(projectCode, new GetCasesFiltersParameter(), 1, 0).getResult.getCount > 0

if (hasCases) {
val results = resultFiles.flatMap { file =>
Using.resource(Source.fromFile(file)) { source =>
Json.parse(source.getLines().mkString("\n")).as[Seq[TestResult]]
}
}

val description = Option(System.getenv(CheckPRRunIdKey))
.map(rId => s"[GitHub checkPR action run details](https://github.com/wavesplatform/Waves/actions/runs/$rId)")
.getOrElse("Local checkPR run")
val title = Option(QaseClient.getConfig.runName()).getOrElse("unknown")

val runId = runsApi
.createRun(
projectCode,
new RunCreate()
.title(title)
.description(description)
.includeAllCases(true)
.isAutotest(true)
)
.getResult
.getId
val resultOps = new ReportersResultOperationsImpl(resultsApi)
results.foreach { result =>
resultOps.addBulkResult(
new ResultCreate()
.status(ResultCreate.StatusEnum.fromValue(result.status))
.comment(result.comment)
.stacktrace(result.stackTrace.orNull)
.caseId(result.caseId)
.timeMs(result.timeMs)
)
}

QaseClient.getConfig.setProperty(RUN_ID_KEY, runId.toString)
QaseClient.getConfig.setProperty(PROJECT_CODE_KEY, projectCode)
resultOps.sendBulkResult()

runsApi.completeRun(projectCode, runId.toInt)
}
}(_.foreach(f => Files.delete(f.toPath)))
}
}

@tailrec
private def waitForActiveRunComplete(runsApi: RunsApi, retries: Int = 10): Unit = {
val hasActiveRuns = QaseProjects.exists { projectCode =>
runsApi.getRuns(projectCode, new GetRunsFiltersParameter().status("active"), 1, 0, "cases").getResult.getCount > 0
}
if (hasActiveRuns && retries > 0) {
Thread.sleep(3000)
waitForActiveRunComplete(runsApi, retries - 1)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,7 @@ class ContractCompilerTest extends PropSpec {
)
}

property("union as argument of non-@Callable function is allowed in V6") {
property("NODE-516. union as argument of non-@Callable function is allowed in V6") {
val script =
"""
|{-# STDLIB_VERSION 6 #-}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ class SetScriptTransactionDiffTest extends PropSpec with WithDomain {
}
}

property("unions are forbidden as @Callable arguments for RIDE 6 scripts and allowed for RIDE 4 and 5") {
property("NODE-242. unions are forbidden as @Callable arguments for RIDE 6 scripts and allowed for RIDE 4 and 5") {
def checkForExpr(expr: String, version: StdLibVersion): Assertion = {
val compileVersion = if (version == V6) V5 else version
val script = ContractScriptImpl(version, TestCompiler(compileVersion).compile(expr).explicitGet())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class DAppListArgTypesTest extends PropSpec with WithDomain with Inside {
private def rideList(args: EVALUATED*) =
List(ARR(Vector(args*), false).explicitGet())

property("can't pass list as callable argument before V4 activation") {
property("NODE-801. can't pass list as callable argument before V4 activation") {
// precompiled to avoid compilation error
val callable = CallableFunction(CallableAnnotation("i"), FUNC("f", List("args"), REF(GlobalValNames.Nil)))
val v3DApp = DApp(DAppMeta(), Nil, List(callable), None)
Expand All @@ -56,7 +56,7 @@ class DAppListArgTypesTest extends PropSpec with WithDomain with Inside {
}
}

property("list as callable argument is allowed after V4 activation") {
property("NODE-800. list as callable argument is allowed after V4 activation") {
val args = rideList(CONST_STRING("value1").explicitGet(), CONST_STRING("value2").explicitGet())
val dApp =
TestCompiler(V4).compileContract(
Expand Down Expand Up @@ -87,7 +87,7 @@ class DAppListArgTypesTest extends PropSpec with WithDomain with Inside {
""".stripMargin
)

property("list as callable argument is checked for primitives after V6 activation") {
property("NODE-241, NODE-243, NODE-244.list as callable argument is checked for primitives after V6 activation") {
val settings =
TestFunctionalitySettings.Enabled
.copy(preActivatedFeatures = Map(Ride4DApps.id -> 0, BlockV5.id -> 0, SynchronousCalls.id -> 0, RideV6F.id -> 3))
Expand Down Expand Up @@ -127,7 +127,7 @@ class DAppListArgTypesTest extends PropSpec with WithDomain with Inside {
assert(forbidAfterActivation = true, rideList(rideList().head))
}

property("list of object as callable argument is forbidden by serialization") {
property("NODE-799. list of object as callable argument is forbidden by serialization") {
val (_, invoke, _) = preconditions(dApp, rideList(CaseObj(Types.UNIT, Map())))
(the[Throwable] thrownBy invoke()).getMessage should include("Serialization of value Unit is unsupported")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ class InvokeScriptTransactionDiffTest extends PropSpec with WithDomain with DBCa
}
}

property("argument passed to callable function has wrong type") {
property("NODE-60. Argument passed to callable function has wrong type") {
val (genesis, setScript, ci) = simplePreconditionsAndSetContract(invocationParamsCount = 2)
testDiff(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci))) {
_ should produceRejectOrFailedDiff("Can't apply (CONST_BOOLEAN) to 'parseInt(str: String)'")
Expand Down Expand Up @@ -952,7 +952,7 @@ class InvokeScriptTransactionDiffTest extends PropSpec with WithDomain with DBCa
}
}

property("Function call args count should be equal @Callable func one") {
property("NODE-112. Function call args count should be equal @Callable func one") {
Seq(0, 3)
.foreach { invocationArgsCount =>
val (genesis, setScript, ci) = simplePreconditionsAndSetContract(invocationArgsCount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ class InvokeScriptTransactionSpecification extends PropSpec {
req.toTx.explicitGet()
}

property(s"can't have more than ${ContractLimits.MaxInvokeScriptArgs} args") {
property(s"NODE-237, NODE-238. can't have more than ${ContractLimits.MaxInvokeScriptArgs} args") {
TxHelpers.invoke(defaultAddress, Some(""), Seq.fill(22)(CONST_LONG(0))).funcCallOpt.get.args.length shouldBe 22
(the[Exception] thrownBy TxHelpers.invoke(defaultAddress, Some(""), Seq.fill(23)(CONST_LONG(0)))).getMessage should include(
"InvokeScript can't have more than 22 arguments"
Expand Down Expand Up @@ -274,7 +274,7 @@ class InvokeScriptTransactionSpecification extends PropSpec {
) should produce("is unsupported")
}

property(s"can't call a func with non native(simple) args - BigInt") {
property(s"NODE-97. can't call a func with non native(simple) args - BigInt") {
val pk = PublicKey.fromBase58String(publicKey).explicitGet()
InvokeScriptTransaction.create(
1.toByte,
Expand Down
Loading

0 comments on commit 25b60c3

Please sign in to comment.