Skip to content

Commit

Permalink
Configurable assertions in Ledger API test tool by feature descriptors (
Browse files Browse the repository at this point in the history
#11328)

* ApiVersionService propagates self-service error codes flag.
* ParticipantTestContext is enriched with feature descriptors
* ContractIdIT adapted with assertions for self-service error codes

CHANGELOG_BEGIN
CHANGELOG_END
  • Loading branch information
tudor-da authored Oct 25, 2021
1 parent 96b7b58 commit 03cfd12
Show file tree
Hide file tree
Showing 20 changed files with 292 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

syntax = "proto3";

package com.daml.ledger.api.v1;

option java_outer_classname = "ExperimentalFeaturesOuterClass";
option java_package = "com.daml.ledger.api.v1";
option csharp_namespace = "Com.Daml.Ledger.Api.V1";

/*
IMPORTANT: in contrast to other parts of the Ledger API, only json-wire backwards
compatibility guarantees are given for the messages in this file.
*/

// See the feature message definitions for descriptions.
message ExperimentalFeatures {
ExperimentalSelfServiceErrorCodes self_service_error_codes = 1;
}

// GRPC self-service error codes are returned by the Ledger API.
message ExperimentalSelfServiceErrorCodes {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ syntax = "proto3";

package com.daml.ledger.api.v1;

import "com/daml/ledger/api/v1/experimental_features.proto";

option java_outer_classname = "VersionServiceOuterClass";
option java_package = "com.daml.ledger.api.v1";
option csharp_namespace = "Com.Daml.Ledger.Api.V1";
Expand All @@ -26,6 +28,25 @@ message GetLedgerApiVersionRequest {

message GetLedgerApiVersionResponse {

// The version of the ledger API
// The version of the ledger API.
string version = 1;

// The features supported by this Ledger API endpoint.
//
// Daml applications CAN use the feature descriptor on top of
// version constraints on the Ledger API version to determine
// whether a given Ledger API endpoint supports the features
// required to run the application.
//
// See the feature descriptions themselves for the relation between
// Ledger API versions and feature presence.
FeaturesDescriptor features = 2;
}

message FeaturesDescriptor {
// Features under development or features that are used
// for ledger implementation testing purposes only.
//
// Daml applications SHOULD not depend on these in production.
ExperimentalFeatures experimental = 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ class RejectionGenerators(conformanceMode: Boolean) {
}
}

// TODO error codes: Remove with the removal of the compatibility constraint from Canton
object RejectionGenerators extends RejectionGenerators(conformanceMode = false)

sealed trait ErrorCauseExport
object ErrorCauseExport {
final case class DamlLf(error: LfError) extends ErrorCauseExport
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package com.daml.error.utils

import com.google.protobuf
import com.google.rpc.{ErrorInfo, RequestInfo, ResourceInfo, RetryInfo}

object ErrorDetails {
sealed trait ErrorDetail extends Product with Serializable

final case class ResourceInfoDetail(name: String, typ: String) extends ErrorDetail
final case class ErrorInfoDetail(reason: String) extends ErrorDetail
final case class RetryInfoDetail(retryDelayInSeconds: Long) extends ErrorDetail
final case class RequestInfoDetail(requestId: String) extends ErrorDetail

def from(anys: Seq[protobuf.Any]): Seq[ErrorDetail] = anys.toList.map {
case any if any.is(classOf[ResourceInfo]) =>
val v = any.unpack(classOf[ResourceInfo])
ResourceInfoDetail(v.getResourceType, v.getResourceName)

case any if any.is(classOf[ErrorInfo]) =>
val v = any.unpack(classOf[ErrorInfo])
ErrorInfoDetail(v.getReason)

case any if any.is(classOf[RetryInfo]) =>
val v = any.unpack(classOf[RetryInfo])
RetryInfoDetail(v.getRetryDelay.getSeconds)

case any if any.is(classOf[RequestInfo]) =>
val v = any.unpack(classOf[RequestInfo])
RequestInfoDetail(v.getRequestId)

case any => throw new IllegalStateException(s"Could not unpack value of: |$any|")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ package com.daml.error

import ch.qos.logback.classic.Level
import com.daml.error.ErrorCategory.TransientServerFailure
import com.daml.error.utils.ErrorDetails
import com.daml.error.utils.testpackage.SeriousError
import com.daml.error.utils.testpackage.subpackage.NotSoSeriousError
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.platform.testing.LogCollector
import com.google.rpc.{ErrorInfo, RequestInfo, RetryInfo}
import io.grpc.protobuf.StatusProto
import org.scalatest.BeforeAndAfter
import org.scalatest.flatspec.AnyFlatSpec
Expand Down Expand Up @@ -71,21 +71,18 @@ class ErrorCodeSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {
val actualTrailers = actualGrpcError.getTrailers
val actualRpcStatus = StatusProto.fromStatusAndTrailers(actualStatus, actualTrailers)

val Seq(rawErrorInfo, rawRetryInfo, rawRequestInfo, rawResourceInfo) =
actualRpcStatus.getDetailsList.asScala.toSeq

val actualResourceInfo = rawResourceInfo.unpack(classOf[com.google.rpc.ResourceInfo])
val actualErrorInfo = rawErrorInfo.unpack(classOf[ErrorInfo])
val actualRetryInfo = rawRetryInfo.unpack(classOf[RetryInfo])
val actualRequestInfo = rawRequestInfo.unpack(classOf[RequestInfo])
val errorDetails =
ErrorDetails.from(actualRpcStatus.getDetailsList.asScala.toSeq)

actualStatus.getCode shouldBe NotSoSeriousError.category.grpcCode.get
actualGrpcError.getMessage shouldBe expectedErrorMessage
actualErrorInfo.getReason shouldBe NotSoSeriousError.id
actualRetryInfo.getRetryDelay.getSeconds shouldBe TransientServerFailure.retryable.get.duration.toSeconds
actualRequestInfo.getRequestId shouldBe correlationId
actualResourceInfo.getResourceType shouldBe error.resources.head._1.asString
actualResourceInfo.getResourceName shouldBe error.resources.head._2

errorDetails should contain theSameElementsAs Seq(
ErrorDetails.ErrorInfoDetail(NotSoSeriousError.id),
ErrorDetails.RetryInfoDetail(TransientServerFailure.retryable.get.duration.toSeconds),
ErrorDetails.RequestInfoDetail(correlationId),
ErrorDetails.ResourceInfoDetail(error.resources.head._1.asString, error.resources.head._2),
)
}

private def logSeriousError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package com.daml

import com.daml.error.utils.ErrorDetails
import com.daml.error.{
ContextualizedErrorLogger,
DamlContextualizedErrorLogger,
Expand All @@ -13,7 +14,6 @@ import com.daml.lf.data.Ref
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.platform.server.api.validation.ErrorFactories
import com.daml.platform.server.api.validation.ErrorFactories._
import com.google.protobuf
import com.google.rpc._
import io.grpc.Status.Code
import io.grpc.StatusRuntimeException
Expand Down Expand Up @@ -492,38 +492,3 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope
// TODO error codes: Assert logging
}
}

object ErrorDetails {

sealed trait ErrorDetail

final case class ResourceInfoDetail(name: String, typ: String) extends ErrorDetail

final case class ErrorInfoDetail(reason: String) extends ErrorDetail

final case class RetryInfoDetail(retryDelayInSeconds: Long) extends ErrorDetail

final case class RequestInfoDetail(requestId: String) extends ErrorDetail

def from(anys: Seq[protobuf.Any]): Seq[ErrorDetail] = {
anys.toList.map(from)
}

private def from(any: protobuf.Any): ErrorDetail = {
if (any.is(classOf[ResourceInfo])) {
val v = any.unpack(classOf[ResourceInfo])
ResourceInfoDetail(v.getResourceType, v.getResourceName)
} else if (any.is(classOf[ErrorInfo])) {
val v = any.unpack(classOf[ErrorInfo])
ErrorInfoDetail(v.getReason)
} else if (any.is(classOf[RetryInfo])) {
val v = any.unpack(classOf[RetryInfo])
RetryInfoDetail(v.getRetryDelay.getSeconds)
} else if (any.is(classOf[RequestInfo])) {
val v = any.unpack(classOf[RequestInfo])
RequestInfoDetail(v.getRequestId)
} else {
throw new IllegalStateException(s"Could not unpack value of: |$any|")
}
}
}
2 changes: 2 additions & 0 deletions ledger/ledger-api-test-tool/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ da_scala_binary(
"//ledger/test-common:model-tests-%s.scala" % lf_version,
"//ledger/test-common:dar-files-%s-lib" % lf_version,
"//ledger-api/grpc-definitions:ledger_api_proto_scala",
"//ledger/error",
"//ledger/ledger-api-common",
"//libs-scala/build-info",
"//libs-scala/grpc-utils",
Expand Down Expand Up @@ -124,6 +125,7 @@ da_scala_binary(
":ledger-api-test-tool-%s-lib" % lf_version,
"//daml-lf/data",
"//language-support/scala/bindings",
"//ledger/error",
"//ledger/ledger-api-common",
"//ledger/ledger-resources",
"//libs-scala/resources",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@

package com.daml.ledger.api.testtool.infrastructure

import java.util.regex.Pattern

import com.daml.error.ErrorCode
import com.daml.error.utils.ErrorDetails
import com.daml.grpc.{GrpcException, GrpcStatus}
import com.daml.ledger.api.testtool.infrastructure.participant.ParticipantTestContext
import com.daml.timer.RetryStrategy
import com.google.rpc.ErrorInfo
import io.grpc.Status
import io.grpc.protobuf.StatusProto
import io.grpc.{Status, StatusRuntimeException}
import munit.{ComparisonFailException, Assertions => MUnit}

import java.util.regex.Pattern
import scala.annotation.tailrec
import scala.concurrent.Future
import scala.jdk.CollectionConverters._
Expand Down Expand Up @@ -44,6 +46,31 @@ object Assertions {
}
}

/** Asserts GRPC error codes depending on the self-service error codes feature in the Ledger API. */
def assertGrpcError(
participant: ParticipantTestContext,
t: Throwable,
expectedCode: Status.Code,
selfServiceErrorCode: ErrorCode,
exceptionMessageSubstring: Option[String],
checkDefiniteAnswerMetadata: Boolean,
): Unit =
if (participant.features.selfServiceErrorCodes)
t match {
case statusRuntimeException: StatusRuntimeException =>
assertSelfServiceErrorCode(statusRuntimeException, selfServiceErrorCode)
case t => fail(s"Throwable $t does not match ErrorCode $selfServiceErrorCode")
}
else {
assertGrpcErrorRegex(
t,
expectedCode,
exceptionMessageSubstring
.map(msgSubstring => Pattern.compile(Pattern.quote(msgSubstring))),
checkDefiniteAnswerMetadata,
)
}

/** A non-regex alternative to [[assertGrpcErrorRegex]] which just does a substring check.
*/
def assertGrpcError(
Expand Down Expand Up @@ -124,4 +151,44 @@ object Assertions {
/** Allows for assertions with more information in the error messages. */
implicit def futureAssertions[T](future: Future[T]): FutureAssertions[T] =
new FutureAssertions[T](future)

def assertSelfServiceErrorCode(
statusRuntimeException: StatusRuntimeException,
expectedErrorCode: ErrorCode,
): Unit = {
val status = StatusProto.fromThrowable(statusRuntimeException)

val expectedStatusCode = expectedErrorCode.category.grpcCode
.map(_.value())
.getOrElse(
throw new RuntimeException(
s"Errors without grpc code cannot be asserted on the Ledger API. Expected error: $expectedErrorCode"
)
)
val expectedErrorId = expectedErrorCode.id
val expectedRetryabilitySeconds = expectedErrorCode.category.retryable.map(_.duration.toSeconds)

val actualStatusCode = status.getCode
val actualErrorDetails = ErrorDetails.from(status.getDetailsList.asScala.toSeq)
val actualErrorId = actualErrorDetails
.collectFirst { case err: ErrorDetails.ErrorInfoDetail => err.reason }
.getOrElse(fail("Actual error id is not defined"))
val actualRetryabilitySeconds = actualErrorDetails
.collectFirst { case err: ErrorDetails.RetryInfoDetail => err.retryDelayInSeconds }

Assertions.assertEquals(
"gRPC error code mismatch",
actualStatusCode,
expectedStatusCode,
)

if (!actualErrorId.contains(expectedErrorId))
fail(s"Actual error id ($actualErrorId) does not match expected error id ($expectedErrorId}")

Assertions.assertEquals(
s"Error retryability details mismatch",
actualRetryabilitySeconds,
expectedRetryabilitySeconds,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import com.daml.ledger.api.v1.testing.time_service.TimeServiceGrpc
import com.daml.ledger.api.v1.testing.time_service.TimeServiceGrpc.TimeService
import com.daml.ledger.api.v1.transaction_service.TransactionServiceGrpc
import com.daml.ledger.api.v1.transaction_service.TransactionServiceGrpc.TransactionService
import com.daml.ledger.api.v1.version_service.VersionServiceGrpc
import com.daml.ledger.api.v1.version_service.VersionServiceGrpc.VersionService
import io.grpc.{Channel, ClientInterceptor}
import io.grpc.health.v1.health.HealthGrpc
import io.grpc.health.v1.health.HealthGrpc.Health
Expand Down Expand Up @@ -80,4 +82,6 @@ private[infrastructure] final class LedgerServices(
val time: TimeService =
TimeServiceGrpc.stub(participant)

val version: VersionService =
VersionServiceGrpc.stub(participant)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ private[infrastructure] final class LedgerSession private (
applicationId,
identifierSuffix,
clientTlsConfiguration,
session.features,
)
}
.map(new LedgerTestContext(_))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ final class LedgerTestCasesRunner(
applicationId = "upload-dars",
identifierSuffix = identifierSuffix,
clientTlsConfiguration = clientTlsConfiguration,
features = session.features,
)
_ <- Future.sequence(Dars.resources.map(uploadDar(context, _)))
} yield ()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package com.daml.ledger.api.testtool.infrastructure.participant

import com.daml.ledger.api.v1.version_service.GetLedgerApiVersionResponse

object Features {
def fromApiVersionResponse(request: GetLedgerApiVersionResponse): Features = {
val selfServiceErrorCodesFeature = for {
features <- request.features
experimental <- features.experimental
_ <- experimental.selfServiceErrorCodes
} yield SelfServiceErrorCodes

Features(selfServiceErrorCodesFeature.toList)
}
}

case class Features(features: Seq[Feature]) {
val selfServiceErrorCodes: Boolean = SelfServiceErrorCodes.enabled(features)
}

sealed trait Feature

sealed trait ExperimentalFeature extends Feature

case object SelfServiceErrorCodes extends ExperimentalFeature {
def enabled(features: Seq[Feature]): Boolean = features.contains(SelfServiceErrorCodes)
}
Loading

0 comments on commit 03cfd12

Please sign in to comment.