Skip to content

Commit

Permalink
daml test-script (digital-asset#3918)
Browse files Browse the repository at this point in the history
* Start on daml test-scripts

* Run all `Script a` as test cases

* LedgerClient: Expose PackageManagementClient

To enable DAR uploads

* Upload the DAR to the ledger

* Start sandbox if no ledger specified

* Format daml test-script

* Fix deprecation warning on ActorMaterializer

* Add test-case //daml-script/tests:test_daml_script_test_runner

* Add daml test-script command

CHANGELOG_BEGIN

- [DAML Script - Experimental] Allow running DAML scripts as test-cases.
  Executing ``daml test-script --dar mydar.dar`` will execute all
  definitions matching the type ``Script a`` as test-cases.
  See `digital-asset#3687 <https://github.com/digital-asset/daml/issues/3687>`__.

CHANGELOG_END

* daml-test-script enable logging

* Remove outdated TODO comment

* daml script-test More elaborate test-caseo

Compare to expected output and add failing test-case

* daml test-script Don't abort on test-failure

Before the test runner would abort on the first failed test-case. This
occasionally introduce additional test-failures if the sandbox was
torn down half-way through execution.

* ./fmt.sh

Co-authored-by: Andreas Herrmann <andreash87@gmx.ch>
  • Loading branch information
aherrmann-da and aherrmann authored Jan 3, 2020
1 parent f8c247c commit 6e25d10
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package com.digitalasset.daml.sdk
import com.digitalasset.daml.lf.engine.trigger.{RunnerMain => Trigger}
import com.digitalasset.daml.lf.engine.script.{RunnerMain => Script}
import com.digitalasset.daml.lf.engine.script.{TestMain => TestScript}
import com.digitalasset.codegen.{CodegenMain => Codegen}
import com.digitalasset.extractor.{Main => Extractor}
import com.digitalasset.http.{Main => JsonApi}
Expand All @@ -17,6 +18,7 @@ object SdkMain {
command match {
case "trigger" => Trigger.main(rest)
case "script" => Script.main(rest)
case "test-script" => TestScript.main(rest)
case "codegen" => Codegen.main(rest)
case "extractor" => Extractor.main(rest)
case "json-api" => JsonApi.main(rest)
Expand Down
9 changes: 9 additions & 0 deletions daml-script/runner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ da_scala_library(
"//ledger-service/lf-value-json",
"//ledger/ledger-api-client",
"//ledger/ledger-api-common",
"//ledger/participant-state",
"//ledger/sandbox",
"@maven//:com_github_scopt_scopt_2_12",
"@maven//:com_typesafe_akka_akka_stream_2_12",
"@maven//:io_spray_spray_json_2_12",
Expand All @@ -42,4 +44,11 @@ da_scala_binary(
deps = [":script-runner-lib"],
)

da_scala_binary(
name = "test-runner",
main_class = "com.digitalasset.daml.lf.engine.script.TestMain",
visibility = ["//visibility:public"],
deps = [":script-runner-lib"],
)

exports_files(["src/main/resources/logback.xml"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package com.digitalasset.daml.lf.engine.script

import java.io.File
import java.time.Duration

import com.digitalasset.platform.services.time.TimeProviderType

case class TestConfig(
darPath: File,
ledgerHost: Option[String],
ledgerPort: Option[Int],
participantConfig: Option[File],
timeProviderType: TimeProviderType,
commandTtl: Duration,
)

object TestConfig {
private val parser = new scopt.OptionParser[TestConfig]("test-script") {
head("test-script")

opt[File]("dar")
.required()
.action((f, c) => c.copy(darPath = f))
.text("Path to the dar file containing the script")

opt[String]("ledger-host")
.optional()
.action((t, c) => c.copy(ledgerHost = Some(t)))
.text("Ledger hostname")

opt[Int]("ledger-port")
.optional()
.action((t, c) => c.copy(ledgerPort = Some(t)))
.text("Ledger port")

opt[File]("participant-config")
.optional()
.action((t, c) => c.copy(participantConfig = Some(t)))
.text("File containing the participant configuration in JSON format")

opt[Unit]('w', "wall-clock-time")
.action { (t, c) =>
c.copy(timeProviderType = TimeProviderType.WallClock)
}
.text("Use wall clock time (UTC). When not provided, static time is used.")

opt[Long]("ttl")
.action { (t, c) =>
c.copy(commandTtl = Duration.ofSeconds(t))
}
.text("TTL in seconds used for commands emitted by the trigger. Defaults to 30s.")

checkConfig(c => {
if (c.ledgerHost.isDefined != c.ledgerPort.isDefined) {
failure("Must specify both --ledger-host and --ledger-port")
} else if (c.ledgerHost.isDefined && c.participantConfig.isDefined) {
failure("Cannot specify both --ledger-host and --participant-config")
} else {
success
}
})
}
def parse(args: Array[String]): Option[TestConfig] =
parser.parse(
args,
TestConfig(
darPath = null,
ledgerHost = None,
ledgerPort = None,
participantConfig = None,
timeProviderType = TimeProviderType.Static,
commandTtl = Duration.ofSeconds(30L),
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package com.digitalasset.daml.lf.engine.script

import java.io.FileInputStream

import akka.actor.ActorSystem
import akka.stream._
import com.typesafe.scalalogging.StrictLogging
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean

import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration.Duration
import scala.io.Source
import scalaz.syntax.traverse._
import spray.json._
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.archive.{Dar, DarReader}
import com.digitalasset.daml.lf.archive.Decode
import com.digitalasset.daml.lf.data.Ref.{Identifier, PackageId, QualifiedName}
import com.digitalasset.daml.lf.language.Ast
import com.digitalasset.daml.lf.language.Ast.Package
import com.digitalasset.daml_lf_dev.DamlLf
import com.digitalasset.grpc.adapter.{AkkaExecutionSequencerPool, ExecutionSequencerFactory}
import com.digitalasset.ledger.api.refinements.ApiTypes.ApplicationId
import com.digitalasset.ledger.client.configuration.{
CommandClientConfiguration,
LedgerClientConfiguration,
LedgerIdRequirement
}
import com.digitalasset.ledger.client.services.commands.CommandUpdater
import com.digitalasset.platform.sandbox.SandboxServer
import com.digitalasset.platform.sandbox.config.SandboxConfig
import com.digitalasset.platform.services.time.TimeProviderType
import com.google.protobuf.ByteString

import scala.util.control.NonFatal
import scala.util.{Failure, Success}

object TestMain extends StrictLogging {

def main(args: Array[String]): Unit = {

TestConfig.parse(args) match {
case None => sys.exit(1)
case Some(config) => {
val encodedDar: Dar[(PackageId, DamlLf.ArchivePayload)] =
DarReader().readArchiveFromFile(config.darPath).get
val dar: Dar[(PackageId, Package)] = encodedDar.map {
case (pkgId, pkgArchive) => Decode.readArchivePayload(pkgId, pkgArchive)
}

val applicationId = ApplicationId("Script Test")
val clientConfig = LedgerClientConfiguration(
applicationId = ApplicationId.unwrap(applicationId),
ledgerIdRequirement = LedgerIdRequirement("", enabled = false),
commandClient = CommandClientConfiguration.default,
sslContext = None
)
val timeProvider: TimeProvider =
config.timeProviderType match {
case TimeProviderType.Static => TimeProvider.Constant(Instant.EPOCH)
case TimeProviderType.WallClock => TimeProvider.UTC
case _ =>
throw new RuntimeException(s"Unexpected TimeProviderType: $config.timeProviderType")
}
val commandUpdater = new CommandUpdater(
timeProviderO = Some(timeProvider),
ttl = config.commandTtl,
overrideTtl = true)

val system: ActorSystem = ActorSystem("ScriptTest")
implicit val sequencer: ExecutionSequencerFactory =
new AkkaExecutionSequencerPool("ScriptTestPool")(system)
implicit val materializer: Materializer = Materializer(system)
implicit val ec: ExecutionContext = system.dispatcher

val runner = new Runner(dar, applicationId, commandUpdater)
val (participantParams, participantCleanup) = config.participantConfig match {
case Some(file) => {
val source = Source.fromFile(file)
val fileContent = try {
source.mkString
} finally {
source.close
}
val jsVal = fileContent.parseJson
import ParticipantsJsonProtocol._
(jsVal.convertTo[Participants[ApiParameters]], () => ())
}
case None =>
val (apiParameters, cleanup) = if (config.ledgerHost.isEmpty) {
val sandboxConfig = SandboxConfig.default.copy(
timeProviderType = config.timeProviderType
)
val sandbox = new SandboxServer(sandboxConfig)
val sandboxClosed = new AtomicBoolean(false)

def closeSandbox(): Unit = {
if (sandboxClosed.compareAndSet(false, true)) sandbox.close()
}

try Runtime.getRuntime.addShutdownHook(new Thread(() => closeSandbox()))
catch {
case NonFatal(t) =>
logger.error(
"Shutting down Sandbox application because of initialization error",
t)
closeSandbox()
}
(ApiParameters("localhost", sandbox.port), () => closeSandbox())
} else {
(ApiParameters(config.ledgerHost.get, config.ledgerPort.get), () => ())
}
(
Participants(
default_participant = Some(apiParameters),
participants = Map.empty,
party_participants = Map.empty),
cleanup)
}

val flow: Future[Boolean] = for {
clients <- Runner.connect(participantParams, clientConfig)
_ <- clients.getParticipant(None) match {
case Left(err) => throw new RuntimeException(err)
case Right(client) =>
client.packageManagementClient.uploadDarFile(
ByteString.readFrom(new FileInputStream(config.darPath)))
}
success = new AtomicBoolean(true)
_ <- Future.sequence {
dar.main._2.modules.flatMap {
case (moduleName, module) =>
module.definitions.collect {
case (name, Ast.DValue(Ast.TApp(Ast.TTyCon(tycon), _), _, _, _))
if tycon == runner.scriptTyCon =>
val testRun: Future[Unit] = for {
_ <- runner.run(
clients,
Identifier(dar.main._1, QualifiedName(moduleName, name)),
None)
} yield ()
// Print test result and remember failure.
testRun.onComplete {
case Failure(exception) =>
success.set(false)
println(s"$moduleName:$name FAILURE ($exception)")
case Success(_) =>
println(s"$moduleName:$name SUCCESS")
}
// Do not abort in case of failure, but complete all test runs.
testRun.recover {
case _ => ()
}
}
}
}
} yield success.get()

flow.onComplete { _ =>
participantCleanup()
system.terminate()
}

if (!Await.result(flow, Duration.Inf)) {
sys.exit(1)
}
}
}
}
}
19 changes: 19 additions & 0 deletions daml-script/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,22 @@ client_server_test(
server_files = ["$(rootpath :script-test.dar)"],
tags = ["exclusive"],
)

sh_test(
name = "test_daml_script_test_runner",
srcs = [":daml-script-test-runner.sh"],
args = [
"$(rootpath //daml-script/runner:test-runner)",
"$(rootpath :script-test.dar)",
"$(POSIX_DIFF)",
"$(POSIX_GREP)",
"$(POSIX_SORT)",
],
data = [
":script-test.dar",
"//daml-script/runner:test-runner",
],
tags = ["exclusive"],
toolchains = ["@rules_sh//sh/posix:make_variables"],
deps = ["@bazel_tools//tools/bash/runfiles"],
)
60 changes: 60 additions & 0 deletions daml-script/test/daml-script-test-runner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Copyright (c) 2020 The DAML Authors. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Copy-pasted from the Bazel Bash runfiles library v2.
set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v2 ---

set -euo pipefail

TEST_RUNNER=$(rlocation $TEST_WORKSPACE/$1)
DAR_FILE=$(rlocation $TEST_WORKSPACE/$2)
DIFF=$3
GREP=$4
SORT=$5

set +e
TEST_OUTPUT="$($TEST_RUNNER --dar=$DAR_FILE 2>&1)"
TEST_RESULT=$?
set -e

echo "-- Runner Output -----------------------" >&2
echo "$TEST_OUTPUT" >&2
echo "----------------------------------------" >&2

FAIL=

if [[ $TEST_RESULT = 0 ]]; then
FAIL=1
echo "Expected non-zero exit-code." >&2
fi

EXPECTED="$($SORT <<'EOF'
MultiTest:multiTest SUCCESS
ScriptExample:test SUCCESS
ScriptTest:failingTest FAILURE (com.digitalasset.daml.lf.speedy.SError$DamlEUserError)
ScriptTest:test0 SUCCESS
ScriptTest:test1 SUCCESS
ScriptTest:test3 SUCCESS
ScriptTest:test4 SUCCESS
ScriptTest:testCreateAndExercise SUCCESS
ScriptTest:testKey SUCCESS
EOF
)"

ACTUAL="$(echo -n "$TEST_OUTPUT" | $GREP "SUCCESS\|FAILURE" | $SORT)"

if ! $DIFF -du0 --label expected <(echo -n "$EXPECTED") --label actual <(echo -n "$ACTUAL") >&2; then
FAIL=1
fi

if [[ $FAIL = 1 ]]; then
exit 1
fi
8 changes: 8 additions & 0 deletions daml-script/test/daml/ScriptTest.daml
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,11 @@ testCreateAndExercise = do
<*> createAndExerciseCmd (C alice 42) GetCValue
<*> exerciseCmd cid GetCValue
pure r

-- Used in daml test-script test-case.
failingTest : Script ()
failingTest = do
alice <- allocateParty "alice"
cid <- submit alice $ createCmd (C alice 42)
submit alice $ exerciseCmd cid ShouldFail
pure ()
Loading

0 comments on commit 6e25d10

Please sign in to comment.