mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
Configurable assertions in Ledger API test tool by feature descriptors (#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
This commit is contained in:
parent
96b7b5812f
commit
03cfd1237c
@ -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 {}
|
@ -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";
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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|")
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -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(
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
package com.daml
|
||||
|
||||
import com.daml.error.utils.ErrorDetails
|
||||
import com.daml.error.{
|
||||
ContextualizedErrorLogger,
|
||||
DamlContextualizedErrorLogger,
|
||||
@ -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
|
||||
@ -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|")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
@ -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",
|
||||
|
@ -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._
|
||||
@ -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(
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -80,4 +82,6 @@ private[infrastructure] final class LedgerServices(
|
||||
val time: TimeService =
|
||||
TimeServiceGrpc.stub(participant)
|
||||
|
||||
val version: VersionService =
|
||||
VersionServiceGrpc.stub(participant)
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ private[infrastructure] final class LedgerSession private (
|
||||
applicationId,
|
||||
identifierSuffix,
|
||||
clientTlsConfiguration,
|
||||
session.features,
|
||||
)
|
||||
}
|
||||
.map(new LedgerTestContext(_))
|
||||
|
@ -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 ()
|
||||
|
@ -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)
|
||||
}
|
@ -13,6 +13,7 @@ import com.daml.ledger.api.testtool.infrastructure.{
|
||||
import com.daml.ledger.api.tls.TlsConfiguration
|
||||
import com.daml.ledger.api.v1.ledger_identity_service.GetLedgerIdentityRequest
|
||||
import com.daml.ledger.api.v1.transaction_service.GetLedgerEndRequest
|
||||
import com.daml.ledger.api.v1.version_service.GetLedgerApiVersionRequest
|
||||
import com.daml.timer.RetryStrategy
|
||||
import io.grpc.ClientInterceptor
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -31,18 +32,21 @@ private[infrastructure] final class ParticipantSession private (
|
||||
// global state of the ledger breaks this assumption, no matter what.
|
||||
ledgerId: String,
|
||||
ledgerEndpoint: Endpoint,
|
||||
val features: Features,
|
||||
)(implicit val executionContext: ExecutionContext) {
|
||||
|
||||
private[testtool] def createInitContext(
|
||||
applicationId: String,
|
||||
identifierSuffix: String,
|
||||
clientTlsConfiguration: Option[TlsConfiguration],
|
||||
features: Features,
|
||||
): Future[ParticipantTestContext] =
|
||||
createTestContext(
|
||||
"init",
|
||||
applicationId,
|
||||
identifierSuffix,
|
||||
clientTlsConfiguration = clientTlsConfiguration,
|
||||
features = features,
|
||||
)
|
||||
|
||||
private[testtool] def createTestContext(
|
||||
@ -50,6 +54,7 @@ private[infrastructure] final class ParticipantSession private (
|
||||
applicationId: String,
|
||||
identifierSuffix: String,
|
||||
clientTlsConfiguration: Option[TlsConfiguration],
|
||||
features: Features,
|
||||
): Future[ParticipantTestContext] =
|
||||
for {
|
||||
end <- services.transaction.getLedgerEnd(new GetLedgerEndRequest(ledgerId)).map(_.getOffset)
|
||||
@ -63,6 +68,7 @@ private[infrastructure] final class ParticipantSession private (
|
||||
partyAllocation = partyAllocation,
|
||||
ledgerEndpoint = ledgerEndpoint,
|
||||
clientTlsConfiguration = clientTlsConfiguration,
|
||||
features = features,
|
||||
)
|
||||
}
|
||||
|
||||
@ -94,12 +100,23 @@ object ParticipantSession {
|
||||
.recoverWith { case NonFatal(exception) =>
|
||||
Future.failed(new Errors.ParticipantConnectionException(exception))
|
||||
}
|
||||
features <-
|
||||
services.version
|
||||
.getLedgerApiVersion(new GetLedgerApiVersionRequest(ledgerId))
|
||||
.map(Features.fromApiVersionResponse)
|
||||
.recover { case failure =>
|
||||
// TODO feature descriptors: Remove once all Ledger API implementations respond successfully on VersionService endpoint
|
||||
logger.warn(
|
||||
s"Failure in retrieving the feature descriptors from the version service: $failure"
|
||||
)
|
||||
Features(Seq.empty)
|
||||
}
|
||||
} yield new ParticipantSession(
|
||||
partyAllocation = partyAllocation,
|
||||
services = services,
|
||||
ledgerId = ledgerId,
|
||||
ledgerEndpoint = participant.endpoint,
|
||||
features = features,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -109,6 +109,7 @@ private[testtool] final class ParticipantTestContext private[participant] (
|
||||
partyAllocation: PartyAllocationConfiguration,
|
||||
val ledgerEndpoint: Endpoint,
|
||||
val clientTlsConfiguration: Option[TlsConfiguration],
|
||||
val features: Features,
|
||||
)(implicit ec: ExecutionContext) {
|
||||
|
||||
import ParticipantTestContext._
|
||||
|
@ -3,16 +3,21 @@
|
||||
|
||||
package com.daml.ledger.api.testtool.suites
|
||||
|
||||
import com.daml.error.definitions.LedgerApiErrors
|
||||
import com.daml.grpc.{GrpcException, GrpcStatus}
|
||||
import com.daml.ledger.api.refinements.ApiTypes.Party
|
||||
import com.daml.ledger.api.testtool.infrastructure.Allocation._
|
||||
import com.daml.ledger.api.testtool.infrastructure.Assertions.{assertGrpcError, fail}
|
||||
import com.daml.ledger.api.testtool.infrastructure.Assertions.{
|
||||
assertGrpcError,
|
||||
assertSelfServiceErrorCode,
|
||||
fail,
|
||||
}
|
||||
import com.daml.ledger.api.testtool.infrastructure.LedgerTestSuite
|
||||
import com.daml.ledger.api.testtool.infrastructure.participant.ParticipantTestContext
|
||||
import com.daml.ledger.api.v1.value.{Record, RecordField, Value}
|
||||
import com.daml.ledger.client.binding.Primitive.ContractId
|
||||
import com.daml.ledger.test.semantic.ContractIdTests._
|
||||
import io.grpc.Status
|
||||
import io.grpc.{Status, StatusRuntimeException}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
@ -53,8 +58,10 @@ final class ContractIdIT extends LedgerTestSuite {
|
||||
case Success(_) if accepted => ()
|
||||
case Failure(err: Throwable) if !accepted =>
|
||||
assertGrpcError(
|
||||
alpha,
|
||||
err,
|
||||
Status.Code.INVALID_ARGUMENT,
|
||||
LedgerApiErrors.PreprocessingErrors.PreprocessingFailed,
|
||||
Some(s"""Illegal Contract ID "$testedCid""""),
|
||||
checkDefiniteAnswerMetadata = true,
|
||||
)
|
||||
@ -81,13 +88,27 @@ final class ContractIdIT extends LedgerTestSuite {
|
||||
)
|
||||
.transformWith(Future.successful)
|
||||
} yield result match {
|
||||
// Assert V1 error code
|
||||
case Failure(GrpcException(GrpcStatus(Status.Code.ABORTED, Some(msg)), _))
|
||||
if msg.contains(s"Contract could not be found with id ContractId($testedCid).") =>
|
||||
if !alpha.features.selfServiceErrorCodes && msg.contains(
|
||||
s"Contract could not be found with id ContractId($testedCid)"
|
||||
) =>
|
||||
Success(())
|
||||
case Success(_) =>
|
||||
Failure(new UnknownError("Unexpected Success"))
|
||||
case otherwise =>
|
||||
otherwise.map(_ => ())
|
||||
|
||||
// Assert self-service error code
|
||||
case Failure(exception: StatusRuntimeException)
|
||||
if alpha.features.selfServiceErrorCodes &&
|
||||
Try(
|
||||
assertSelfServiceErrorCode(
|
||||
statusRuntimeException = exception,
|
||||
expectedErrorCode =
|
||||
LedgerApiErrors.InterpreterErrors.LookupErrors.ContractNotFound,
|
||||
)
|
||||
).isSuccess =>
|
||||
Success(())
|
||||
|
||||
case Success(_) => Failure(new UnknownError("Unexpected Success"))
|
||||
case otherwise => otherwise.map(_ => ())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,6 +162,25 @@ conformance_test(
|
||||
],
|
||||
)
|
||||
|
||||
# WIP Full conformance test asserting the Ledger API returning self-service error codes
|
||||
conformance_test(
|
||||
name = "conformance-test-self-service-error-codes",
|
||||
ports = [6865],
|
||||
server = ":app",
|
||||
server_args = [
|
||||
"--contract-id-seeding=testing-weak",
|
||||
"--participant=participant-id=example,port=6865",
|
||||
"--max-deduplication-duration=PT5S",
|
||||
"--index-append-only-schema",
|
||||
"--mutable-contract-state-cache",
|
||||
"--use-self-service-error-codes",
|
||||
],
|
||||
test_tool_args = [
|
||||
# TODO error codes: Assert all suites as from the `conformance-test`
|
||||
"--include=ContractIdIT:RejectV0,ContractIdIT:AcceptSuffixedV1,ContractIdIT:AcceptNonSuffixedV1",
|
||||
],
|
||||
)
|
||||
|
||||
# Feature test: --daml-lf-min-version-1.14-unsafe
|
||||
# This asserts that you can use the 1.14 packages despite their dependencies on a few stable packages in older LF versions.
|
||||
conformance_test(
|
||||
|
@ -154,7 +154,7 @@ private[daml] object ApiServices {
|
||||
ApiLedgerIdentityService.create(() => identityService.getLedgerId(), errorsVersionsSwitcher)
|
||||
|
||||
val apiVersionService =
|
||||
ApiVersionService.create(errorsVersionsSwitcher)
|
||||
ApiVersionService.create(enableSelfServiceErrorCodes)
|
||||
|
||||
val apiPackageService =
|
||||
ApiPackageService.create(ledgerId, packagesService, errorsVersionsSwitcher)
|
||||
|
@ -110,9 +110,6 @@ private[apiserver] final class ApiSubmissionService private[services] (
|
||||
with AutoCloseable {
|
||||
|
||||
private val logger = ContextualizedLogger.get(this.getClass)
|
||||
|
||||
// TODO error codes: review conformance mode usages wherever RejectionGenerators is instantiated
|
||||
private val rejectionGenerators = new RejectionGenerators(conformanceMode = true)
|
||||
private val errorFactories = ErrorFactories(errorCodesVersionSwitcher)
|
||||
|
||||
override def submit(
|
||||
@ -335,7 +332,7 @@ private[apiserver] final class ApiSubmissionService private[services] (
|
||||
|
||||
errorCodesVersionSwitcher.chooseAsFailedFuture(
|
||||
v1 = toStatusExceptionV1(error),
|
||||
v2 = rejectionGenerators
|
||||
v2 = RejectionGenerators
|
||||
.commandExecutorError(cause = ErrorCauseExport.fromErrorCause(error)),
|
||||
)
|
||||
}
|
||||
|
@ -8,8 +8,13 @@ import com.daml.error.{
|
||||
DamlContextualizedErrorLogger,
|
||||
ErrorCodesVersionSwitcher,
|
||||
}
|
||||
import com.daml.ledger.api.v1.experimental_features.{
|
||||
ExperimentalFeatures,
|
||||
ExperimentalSelfServiceErrorCodes,
|
||||
}
|
||||
import com.daml.ledger.api.v1.version_service.VersionServiceGrpc.VersionService
|
||||
import com.daml.ledger.api.v1.version_service.{
|
||||
FeaturesDescriptor,
|
||||
GetLedgerApiVersionRequest,
|
||||
GetLedgerApiVersionResponse,
|
||||
VersionServiceGrpc,
|
||||
@ -24,16 +29,15 @@ import scala.io.Source
|
||||
import scala.util.Try
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
private[apiserver] final class ApiVersionService private (
|
||||
errorCodesVersionSwitcher: ErrorCodesVersionSwitcher
|
||||
)(implicit
|
||||
private[apiserver] final class ApiVersionService private (enableSelfServiceErrorCodes: Boolean)(
|
||||
implicit
|
||||
loggingContext: LoggingContext,
|
||||
ec: ExecutionContext,
|
||||
) extends VersionService
|
||||
with GrpcApiService {
|
||||
|
||||
private val errorCodesVersionSwitcher = new ErrorCodesVersionSwitcher(enableSelfServiceErrorCodes)
|
||||
private val logger = ContextualizedLogger.get(this.getClass)
|
||||
|
||||
private val errorFactories = ErrorFactories(errorCodesVersionSwitcher)
|
||||
private implicit val contextualizedErrorLogger: ContextualizedErrorLogger =
|
||||
new DamlContextualizedErrorLogger(logger, loggingContext, None)
|
||||
@ -46,12 +50,21 @@ private[apiserver] final class ApiVersionService private (
|
||||
): Future[GetLedgerApiVersionResponse] =
|
||||
Future
|
||||
.fromTry(apiVersion)
|
||||
.map(GetLedgerApiVersionResponse(_))
|
||||
.map(apiVersionResponse)
|
||||
.andThen(logger.logErrorsOnCall[GetLedgerApiVersionResponse])
|
||||
.recoverWith { case NonFatal(_) =>
|
||||
internalError
|
||||
}
|
||||
|
||||
private def apiVersionResponse(version: String) =
|
||||
if (enableSelfServiceErrorCodes)
|
||||
GetLedgerApiVersionResponse(version).withFeatures(
|
||||
FeaturesDescriptor().withExperimental(
|
||||
ExperimentalFeatures().withSelfServiceErrorCodes(ExperimentalSelfServiceErrorCodes())
|
||||
)
|
||||
)
|
||||
else GetLedgerApiVersionResponse(version)
|
||||
|
||||
private lazy val internalError: Future[Nothing] =
|
||||
Future.failed(
|
||||
errorFactories.versionServiceInternalError(message = "Cannot read Ledger API version")
|
||||
@ -75,7 +88,7 @@ private[apiserver] final class ApiVersionService private (
|
||||
|
||||
private[apiserver] object ApiVersionService {
|
||||
def create(
|
||||
errorCodesVersionSwitcher: ErrorCodesVersionSwitcher
|
||||
enableSelfServiceErrorCodes: Boolean
|
||||
)(implicit loggingContext: LoggingContext, ec: ExecutionContext): ApiVersionService =
|
||||
new ApiVersionService(errorCodesVersionSwitcher)
|
||||
new ApiVersionService(enableSelfServiceErrorCodes)
|
||||
}
|
||||
|
@ -248,7 +248,7 @@ class ApiSubmissionServiceSpec
|
||||
LfError.Interpretation.DamlException(LfInterpretationError.ContractNotFound("#cid")),
|
||||
None,
|
||||
)
|
||||
) -> ((Status.ABORTED, Status.ABORTED)),
|
||||
) -> ((Status.ABORTED, Status.NOT_FOUND)),
|
||||
ErrorCause.DamlLf(
|
||||
LfError.Interpretation(
|
||||
LfError.Interpretation.DamlException(
|
||||
|
Loading…
Reference in New Issue
Block a user