From 03cfd1237cf9bf3d6cd16d94ad1b195c26c0dacc Mon Sep 17 00:00:00 2001 From: tudor-da Date: Mon, 25 Oct 2021 14:13:44 +0200 Subject: [PATCH] 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 --- .../ledger/api/v1/experimental_features.proto | 23 ++++++ .../daml/ledger/api/v1/version_service.proto | 23 +++++- .../definitions/RejectionGenerators.scala | 3 + .../com/daml/error/utils/ErrorDetails.scala | 36 +++++++++ .../scala/com/daml/error/ErrorCodeSpec.scala | 23 +++--- .../api/validation/ErrorFactoriesSpec.scala | 37 +--------- ledger/ledger-api-test-tool/BUILD.bazel | 2 + .../testtool/infrastructure/Assertions.scala | 73 ++++++++++++++++++- .../infrastructure/LedgerServices.scala | 4 + .../infrastructure/LedgerSession.scala | 1 + .../LedgerTestCasesRunner.scala | 1 + .../participant/ParticipantFeature.scala | 30 ++++++++ .../participant/ParticipantSession.scala | 19 ++++- .../participant/ParticipantTestContext.scala | 1 + .../api/testtool/suites/ContractIdIT.scala | 35 +++++++-- ledger/ledger-on-memory/BUILD.bazel | 19 +++++ .../platform/apiserver/ApiServices.scala | 2 +- .../services/ApiSubmissionService.scala | 5 +- .../services/ApiVersionService.scala | 27 +++++-- .../services/ApiSubmissionServiceSpec.scala | 2 +- 20 files changed, 292 insertions(+), 74 deletions(-) create mode 100644 ledger-api/grpc-definitions/com/daml/ledger/api/v1/experimental_features.proto create mode 100644 ledger/error/src/main/scala/com/daml/error/utils/ErrorDetails.scala create mode 100644 ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantFeature.scala diff --git a/ledger-api/grpc-definitions/com/daml/ledger/api/v1/experimental_features.proto b/ledger-api/grpc-definitions/com/daml/ledger/api/v1/experimental_features.proto new file mode 100644 index 0000000000..cf1fb659a5 --- /dev/null +++ b/ledger-api/grpc-definitions/com/daml/ledger/api/v1/experimental_features.proto @@ -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 {} diff --git a/ledger-api/grpc-definitions/com/daml/ledger/api/v1/version_service.proto b/ledger-api/grpc-definitions/com/daml/ledger/api/v1/version_service.proto index 915f69bba6..b0e84ee43e 100644 --- a/ledger-api/grpc-definitions/com/daml/ledger/api/v1/version_service.proto +++ b/ledger-api/grpc-definitions/com/daml/ledger/api/v1/version_service.proto @@ -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; } diff --git a/ledger/error/src/main/scala/com/daml/error/definitions/RejectionGenerators.scala b/ledger/error/src/main/scala/com/daml/error/definitions/RejectionGenerators.scala index 42d24af9ec..ed1be44466 100644 --- a/ledger/error/src/main/scala/com/daml/error/definitions/RejectionGenerators.scala +++ b/ledger/error/src/main/scala/com/daml/error/definitions/RejectionGenerators.scala @@ -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 diff --git a/ledger/error/src/main/scala/com/daml/error/utils/ErrorDetails.scala b/ledger/error/src/main/scala/com/daml/error/utils/ErrorDetails.scala new file mode 100644 index 0000000000..2ef7af2224 --- /dev/null +++ b/ledger/error/src/main/scala/com/daml/error/utils/ErrorDetails.scala @@ -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|") + } +} diff --git a/ledger/error/src/test/suite/scala/com/daml/error/ErrorCodeSpec.scala b/ledger/error/src/test/suite/scala/com/daml/error/ErrorCodeSpec.scala index eac93cf3ae..1f24e42639 100644 --- a/ledger/error/src/test/suite/scala/com/daml/error/ErrorCodeSpec.scala +++ b/ledger/error/src/test/suite/scala/com/daml/error/ErrorCodeSpec.scala @@ -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( diff --git a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala index 9d6fcf8e49..500454bc41 100644 --- a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala +++ b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala @@ -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|") - } - } -} diff --git a/ledger/ledger-api-test-tool/BUILD.bazel b/ledger/ledger-api-test-tool/BUILD.bazel index c296a39586..760802ef9f 100644 --- a/ledger/ledger-api-test-tool/BUILD.bazel +++ b/ledger/ledger-api-test-tool/BUILD.bazel @@ -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", diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/Assertions.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/Assertions.scala index 42374a5c1e..8d18a46343 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/Assertions.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/Assertions.scala @@ -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, + ) + } } diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala index 39de4fff97..43c08b36de 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerServices.scala @@ -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) } diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerSession.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerSession.scala index f84eb2e859..6d09d9dfe2 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerSession.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerSession.scala @@ -28,6 +28,7 @@ private[infrastructure] final class LedgerSession private ( applicationId, identifierSuffix, clientTlsConfiguration, + session.features, ) } .map(new LedgerTestContext(_)) diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestCasesRunner.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestCasesRunner.scala index 0a81fe0b8b..c5280483a3 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestCasesRunner.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/LedgerTestCasesRunner.scala @@ -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 () diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantFeature.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantFeature.scala new file mode 100644 index 0000000000..e2e8c32aed --- /dev/null +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantFeature.scala @@ -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) +} diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantSession.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantSession.scala index 3bc537e011..234d3c6c34 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantSession.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantSession.scala @@ -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, ) } - } diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala index 9c502837b6..3ee8df98e5 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/infrastructure/participant/ParticipantTestContext.scala @@ -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._ diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/ContractIdIT.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/ContractIdIT.scala index 4bc1a8c9c5..4fcc5a3706 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/ContractIdIT.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/ContractIdIT.scala @@ -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(_ => ()) } } diff --git a/ledger/ledger-on-memory/BUILD.bazel b/ledger/ledger-on-memory/BUILD.bazel index 95807bce4a..6a0363dc3c 100644 --- a/ledger/ledger-on-memory/BUILD.bazel +++ b/ledger/ledger-on-memory/BUILD.bazel @@ -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( diff --git a/ledger/participant-integration-api/src/main/scala/platform/apiserver/ApiServices.scala b/ledger/participant-integration-api/src/main/scala/platform/apiserver/ApiServices.scala index f1d76322ea..22e332b436 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/apiserver/ApiServices.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/apiserver/ApiServices.scala @@ -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) diff --git a/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala b/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala index 52c3975023..2892eb28f6 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala @@ -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)), ) } diff --git a/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiVersionService.scala b/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiVersionService.scala index 0e13fc4502..999c5f1611 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiVersionService.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiVersionService.scala @@ -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) } diff --git a/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala b/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala index a6b94723c9..7d89cbca62 100644 --- a/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala +++ b/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala @@ -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(